diff options
author | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:05:23 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:05:23 +0100 |
commit | dc28c07fa526841e05e16161c74a6c23984f1dd5 (patch) | |
tree | d68796bc7708eba33fbf7247e1a92188ac5acf6f /Userland/Applications/SoundPlayer | |
parent | aa939c4b4b8a7eb1d22b166ebb5fb737d6e66714 (diff) | |
download | serenity-dc28c07fa526841e05e16161c74a6c23984f1dd5.zip |
Applications: Move to Userland/Applications/
Diffstat (limited to 'Userland/Applications/SoundPlayer')
-rw-r--r-- | Userland/Applications/SoundPlayer/CMakeLists.txt | 9 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/PlaybackManager.cpp | 188 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/PlaybackManager.h | 77 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/SampleWidget.cpp | 82 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/SampleWidget.h | 47 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp | 194 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/SoundPlayerWidget.h | 94 | ||||
-rw-r--r-- | Userland/Applications/SoundPlayer/main.cpp | 109 |
8 files changed, 800 insertions, 0 deletions
diff --git a/Userland/Applications/SoundPlayer/CMakeLists.txt b/Userland/Applications/SoundPlayer/CMakeLists.txt new file mode 100644 index 0000000000..99e1da4653 --- /dev/null +++ b/Userland/Applications/SoundPlayer/CMakeLists.txt @@ -0,0 +1,9 @@ +set(SOURCES + main.cpp + PlaybackManager.cpp + SampleWidget.cpp + SoundPlayerWidget.cpp +) + +serenity_app(SoundPlayer ICON app-sound-player) +target_link_libraries(SoundPlayer LibAudio LibGUI) diff --git a/Userland/Applications/SoundPlayer/PlaybackManager.cpp b/Userland/Applications/SoundPlayer/PlaybackManager.cpp new file mode 100644 index 0000000000..1421e75215 --- /dev/null +++ b/Userland/Applications/SoundPlayer/PlaybackManager.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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 "PlaybackManager.h" + +PlaybackManager::PlaybackManager(NonnullRefPtr<Audio::ClientConnection> connection) + : m_connection(connection) +{ + m_timer = Core::Timer::construct(100, [&]() { + if (!m_loader) + return; + next_buffer(); + }); + m_timer->stop(); +} + +PlaybackManager::~PlaybackManager() +{ +} + +void PlaybackManager::set_loader(NonnullRefPtr<Audio::Loader>&& loader) +{ + stop(); + m_loader = loader; + if (m_loader) { + m_total_length = m_loader->total_samples() / static_cast<float>(m_loader->sample_rate()); + m_timer->start(); + load_next_buffer(); + } else { + m_timer->stop(); + } +} + +void PlaybackManager::stop() +{ + set_paused(true); + m_connection->clear_buffer(true); + m_buffers.clear(); + m_last_seek = 0; + m_next_buffer = nullptr; + m_current_buffer = nullptr; + m_next_ptr = 0; + + if (m_loader) + m_loader->reset(); +} + +void PlaybackManager::play() +{ + set_paused(false); +} + +void PlaybackManager::loop(bool loop) +{ + m_loop = loop; +} + +void PlaybackManager::seek(const int position) +{ + if (!m_loader) + return; + + 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->shbuf_id(); + + if (id >= 0 && id != current_id) { + while (!m_buffers.is_empty()) { + --m_next_ptr; + auto buffer = m_buffers.take_first(); + + if (buffer->shbuf_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 && m_loader) + 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) { + dbgln("Exhausted samples :^)"); + if (m_loop) + seek(0); + else + stop(); + } + + return; + } + + bool enqueued = m_connection->try_enqueue(*m_next_buffer); + if (!enqueued) + return; + + load_next_buffer(); +} diff --git a/Userland/Applications/SoundPlayer/PlaybackManager.h b/Userland/Applications/SoundPlayer/PlaybackManager.h new file mode 100644 index 0000000000..35be78fe9d --- /dev/null +++ b/Userland/Applications/SoundPlayer/PlaybackManager.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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/Vector.h> +#include <LibAudio/Buffer.h> +#include <LibAudio/ClientConnection.h> +#include <LibAudio/Loader.h> +#include <LibCore/Timer.h> + +#define PLAYBACK_MANAGER_BUFFER_SIZE 64 * KiB +#define PLAYBACK_MANAGER_RATE 44100 + +class PlaybackManager final { +public: + PlaybackManager(NonnullRefPtr<Audio::ClientConnection>); + ~PlaybackManager(); + + void play(); + void stop(); + void pause(); + void seek(const int position); + void loop(bool); + bool toggle_pause(); + void set_loader(NonnullRefPtr<Audio::Loader>&&); + + int last_seek() const { return m_last_seek; } + bool is_paused() const { return m_paused; } + float total_length() const { return m_total_length; } + RefPtr<Audio::Buffer> current_buffer() const { return m_current_buffer; } + + NonnullRefPtr<Audio::ClientConnection> connection() const { return m_connection; } + + Function<void()> on_update; + +private: + void next_buffer(); + void set_paused(bool); + void load_next_buffer(); + void remove_dead_buffers(); + + bool m_paused { true }; + bool m_loop = { false }; + size_t m_next_ptr { 0 }; + size_t m_last_seek { 0 }; + float m_total_length { 0 }; + RefPtr<Audio::Loader> m_loader { nullptr }; + NonnullRefPtr<Audio::ClientConnection> m_connection; + RefPtr<Audio::Buffer> m_next_buffer; + RefPtr<Audio::Buffer> m_current_buffer; + Vector<RefPtr<Audio::Buffer>> m_buffers; + RefPtr<Core::Timer> m_timer; +}; diff --git a/Userland/Applications/SoundPlayer/SampleWidget.cpp b/Userland/Applications/SoundPlayer/SampleWidget.cpp new file mode 100644 index 0000000000..b3884a175a --- /dev/null +++ b/Userland/Applications/SoundPlayer/SampleWidget.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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 "SampleWidget.h" +#include <LibAudio/Buffer.h> +#include <LibGUI/Painter.h> +#include <math.h> + +SampleWidget::SampleWidget() +{ +} + +SampleWidget::~SampleWidget() +{ +} + +void SampleWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Frame::paint_event(event); + GUI::Painter painter(*this); + + painter.add_clip_rect(event.rect()); + painter.fill_rect(frame_inner_rect(), Color::Black); + + 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(); + + 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((float)m_buffer->samples()[sample_index].left); + + sample_max = max(sample, sample_max); + ++count; + + if (count >= samples_per_pixel) { + Gfx::IntPoint min_point = { x, y_offset + static_cast<int>(-sample_max * frame_inner_rect().height() / 2) }; + Gfx::IntPoint max_point = { x++, y_offset + static_cast<int>(sample_max * frame_inner_rect().height() / 2) }; + painter.draw_line(min_point, max_point, Color::Green); + + count = 0; + sample_max = 0; + } + } + } else { + painter.draw_line({ x, y_offset }, { frame_inner_rect().width(), y_offset }, Color::Green); + } +} + +void SampleWidget::set_buffer(Audio::Buffer* buffer) +{ + if (m_buffer == buffer) + return; + m_buffer = buffer; + update(); +} diff --git a/Userland/Applications/SoundPlayer/SampleWidget.h b/Userland/Applications/SoundPlayer/SampleWidget.h new file mode 100644 index 0000000000..7da9c20b38 --- /dev/null +++ b/Userland/Applications/SoundPlayer/SampleWidget.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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 <LibGUI/Frame.h> + +namespace Audio { +class Buffer; +} + +class SampleWidget final : public GUI::Frame { + C_OBJECT(SampleWidget) +public: + virtual ~SampleWidget() override; + + void set_buffer(Audio::Buffer*); + +private: + SampleWidget(); + virtual void paint_event(GUI::PaintEvent&) override; + + RefPtr<Audio::Buffer> m_buffer; +}; diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp b/Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp new file mode 100644 index 0000000000..f2066aca89 --- /dev/null +++ b/Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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 "SoundPlayerWidget.h" +#include <AK/StringBuilder.h> +#include <LibCore/MimeData.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/Label.h> +#include <LibGUI/MessageBox.h> +#include <math.h> + +SoundPlayerWidget::SoundPlayerWidget(GUI::Window& window, NonnullRefPtr<Audio::ClientConnection> connection) + : m_window(window) + , m_connection(connection) + , m_manager(connection) +{ + set_fill_with_background_color(true); + set_layout<GUI::VerticalBoxLayout>(); + layout()->set_margins({ 2, 2, 2, 2 }); + + auto& status_widget = add<GUI::Widget>(); + status_widget.set_fill_with_background_color(true); + status_widget.set_layout<GUI::HorizontalBoxLayout>(); + + m_elapsed = status_widget.add<GUI::Label>(); + m_elapsed->set_frame_shape(Gfx::FrameShape::Container); + m_elapsed->set_frame_shadow(Gfx::FrameShadow::Sunken); + m_elapsed->set_frame_thickness(2); + m_elapsed->set_fixed_width(80); + + auto& sample_widget_container = status_widget.add<GUI::Widget>(); + sample_widget_container.set_layout<GUI::HorizontalBoxLayout>(); + + m_sample_widget = sample_widget_container.add<SampleWidget>(); + + m_remaining = status_widget.add<GUI::Label>(); + m_remaining->set_frame_shape(Gfx::FrameShape::Container); + m_remaining->set_frame_shadow(Gfx::FrameShadow::Sunken); + m_remaining->set_frame_thickness(2); + m_remaining->set_fixed_width(80); + + m_slider = add<Slider>(Orientation::Horizontal); + m_slider->set_min(0); + m_slider->set_enabled(false); + m_slider->on_knob_released = [&](int value) { m_manager.seek(denormalize_rate(value)); }; + + auto& control_widget = add<GUI::Widget>(); + control_widget.set_fill_with_background_color(true); + control_widget.set_layout<GUI::HorizontalBoxLayout>(); + control_widget.set_fixed_height(30); + control_widget.layout()->set_margins({ 10, 2, 10, 2 }); + control_widget.layout()->set_spacing(10); + + m_play = control_widget.add<GUI::Button>(); + m_play->set_icon(*m_pause_icon); + m_play->set_enabled(false); + m_play->on_click = [this](auto) { + m_play->set_icon(m_manager.toggle_pause() ? *m_play_icon : *m_pause_icon); + }; + + m_stop = control_widget.add<GUI::Button>(); + m_stop->set_enabled(false); + m_stop->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/stop.png")); + m_stop->on_click = [this](auto) { m_manager.stop(); }; + + m_status = add<GUI::Label>(); + m_status->set_frame_shape(Gfx::FrameShape::Box); + m_status->set_frame_shadow(Gfx::FrameShadow::Raised); + m_status->set_frame_thickness(4); + m_status->set_text_alignment(Gfx::TextAlignment::CenterLeft); + m_status->set_fixed_height(18); + m_status->set_text("No file open!"); + + update_position(0); + + m_manager.on_update = [&]() { update_ui(); }; +} + +SoundPlayerWidget::~SoundPlayerWidget() +{ +} + +SoundPlayerWidget::Slider::~Slider() +{ +} + +void SoundPlayerWidget::hide_scope(bool hide) +{ + m_sample_widget->set_visible(!hide); +} + +void SoundPlayerWidget::open_file(String path) +{ + 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(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_sample_ratio = PLAYBACK_MANAGER_RATE / static_cast<float>(loader->sample_rate()); + + m_slider->set_max(normalize_rate(static_cast<int>(loader->total_samples()))); + m_slider->set_enabled(true); + m_play->set_enabled(true); + m_stop->set_enabled(true); + + m_window.set_title(String::formatted("{} - SoundPlayer", loader->file()->filename())); + m_status->set_text(String::formatted( + "Sample rate {}Hz, {} channel(s), {} bits per sample", + loader->sample_rate(), + loader->num_channels(), + loader->bits_per_sample())); + + m_manager.set_loader(move(loader)); + update_position(0); +} + +void SoundPlayerWidget::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()); + } +} + +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::formatted( + "Elapsed:\n{}:{:02}.{:02}", + static_cast<int>(seconds / 60), + static_cast<int>(seconds) % 60, + static_cast<int>(seconds * 100) % 100)); + + m_remaining->set_text(String::formatted( + "Remaining:\n{}:{:02}.{:02}", + 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); +} diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidget.h b/Userland/Applications/SoundPlayer/SoundPlayerWidget.h new file mode 100644 index 0000000000..ab7bc60aea --- /dev/null +++ b/Userland/Applications/SoundPlayer/SoundPlayerWidget.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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 "PlaybackManager.h" +#include "SampleWidget.h" +#include <LibGUI/Button.h> +#include <LibGUI/Label.h> +#include <LibGUI/Slider.h> +#include <LibGUI/Widget.h> +#include <LibGUI/Window.h> + +class SoundPlayerWidget final : public GUI::Widget { + C_OBJECT(SoundPlayerWidget) +public: + virtual ~SoundPlayerWidget() override; + void open_file(String path); + void hide_scope(bool); + PlaybackManager& manager() { return m_manager; } + +private: + explicit SoundPlayerWidget(GUI::Window&, NonnullRefPtr<Audio::ClientConnection>); + + virtual void drop_event(GUI::DropEvent&) override; + + void update_position(const int position); + void update_ui(); + int normalize_rate(int) const; + int denormalize_rate(int) const; + + class Slider final : public GUI::Slider { + C_OBJECT(Slider) + public: + virtual ~Slider() override; + Function<void(int)> on_knob_released; + void set_value(int value) + { + if (!knob_dragging()) + GUI::Slider::set_value(value); + } + + protected: + Slider(Orientation orientation) + : GUI::Slider(orientation) + { + } + + virtual void mouseup_event(GUI::MouseEvent& event) override + { + if (on_knob_released && is_enabled()) + on_knob_released(value()); + + GUI::Slider::mouseup_event(event); + } + }; + + GUI::Window& m_window; + NonnullRefPtr<Audio::ClientConnection> m_connection; + PlaybackManager m_manager; + float m_sample_ratio { 1.0 }; + RefPtr<GUI::Label> m_status; + RefPtr<GUI::Label> m_elapsed; + RefPtr<GUI::Label> m_remaining; + RefPtr<Slider> m_slider; + RefPtr<SampleWidget> m_sample_widget; + RefPtr<Gfx::Bitmap> m_play_icon { Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png") }; + RefPtr<Gfx::Bitmap> m_pause_icon { Gfx::Bitmap::load_from_file("/res/icons/16x16/pause.png") }; + RefPtr<GUI::Button> m_play; + RefPtr<GUI::Button> m_stop; +}; diff --git a/Userland/Applications/SoundPlayer/main.cpp b/Userland/Applications/SoundPlayer/main.cpp new file mode 100644 index 0000000000..47dbd555c8 --- /dev/null +++ b/Userland/Applications/SoundPlayer/main.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * 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 "SoundPlayerWidget.h" +#include <LibAudio/ClientConnection.h> +#include <LibGUI/Action.h> +#include <LibGUI/Application.h> +#include <LibGUI/FilePicker.h> +#include <LibGUI/Menu.h> +#include <LibGUI/MenuBar.h> +#include <LibGUI/Window.h> +#include <LibGfx/CharacterBitmap.h> +#include <stdio.h> + +int main(int argc, char** argv) +{ + if (pledge("stdio shared_buffer accept rpath thread unix cpath fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto app = GUI::Application::construct(argc, argv); + + if (pledge("stdio shared_buffer accept rpath thread unix", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto audio_client = Audio::ClientConnection::construct(); + audio_client->handshake(); + + if (pledge("stdio shared_buffer accept rpath thread", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto app_icon = GUI::Icon::default_icon("app-sound-player"); + + auto window = GUI::Window::construct(); + window->set_title("Sound Player"); + window->set_resizable(false); + window->resize(350, 140); + window->set_icon(app_icon.bitmap_for_size(16)); + + auto menubar = GUI::MenuBar::construct(); + auto& app_menu = menubar->add_menu("Sound Player"); + auto& player = window->set_main_widget<SoundPlayerWidget>(window, audio_client); + + if (argc > 1) { + String path = argv[1]; + player.open_file(path); + player.manager().play(); + } + + auto hide_scope = GUI::Action::create_checkable("Hide scope", { Mod_Ctrl, Key_H }, [&](auto& action) { + player.hide_scope(action.is_checked()); + }); + + app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) { + Optional<String> path = GUI::FilePicker::get_open_filepath(window, "Open sound file..."); + if (path.has_value()) { + player.open_file(path.value()); + } + })); + app_menu.add_action(move(hide_scope)); + app_menu.add_separator(); + app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { + app->quit(); + })); + + auto& playback_menu = menubar->add_menu("Playback"); + + auto loop = GUI::Action::create_checkable("Loop", { Mod_Ctrl, Key_R }, [&](auto& action) { + player.manager().loop(action.is_checked()); + }); + + playback_menu.add_action(move(loop)); + + auto& help_menu = menubar->add_menu("Help"); + help_menu.add_action(GUI::CommonActions::make_about_action("Sound Player", app_icon, window)); + + app->set_menubar(move(menubar)); + + window->show(); + return app->exec(); +} |