From 74f1f2b5e2ed9b06f7dc5c4a247c8512c8b3ec32 Mon Sep 17 00:00:00 2001 From: JJ Roberts-White Date: Sun, 11 Jul 2021 12:58:17 +1000 Subject: [PATCH] Piano: Add Play/Pause, Forward and Back buttons Piano now has a toolbar allowing the playback to be paused, or to be stepped forward or back a note. --- .../Applications/Piano/AudioPlayerLoop.cpp | 62 +++++++++++++++ Userland/Applications/Piano/AudioPlayerLoop.h | 38 +++++++++ Userland/Applications/Piano/CMakeLists.txt | 2 + Userland/Applications/Piano/MainWidget.cpp | 6 +- Userland/Applications/Piano/MainWidget.h | 7 +- Userland/Applications/Piano/PlayerWidget.cpp | 63 +++++++++++++++ Userland/Applications/Piano/PlayerWidget.h | 33 ++++++++ Userland/Applications/Piano/TrackManager.cpp | 12 +++ Userland/Applications/Piano/TrackManager.h | 3 + Userland/Applications/Piano/main.cpp | 77 +++---------------- 10 files changed, 234 insertions(+), 69 deletions(-) create mode 100644 Userland/Applications/Piano/AudioPlayerLoop.cpp create mode 100644 Userland/Applications/Piano/AudioPlayerLoop.h create mode 100644 Userland/Applications/Piano/PlayerWidget.cpp create mode 100644 Userland/Applications/Piano/PlayerWidget.h diff --git a/Userland/Applications/Piano/AudioPlayerLoop.cpp b/Userland/Applications/Piano/AudioPlayerLoop.cpp new file mode 100644 index 0000000000..ea39f00d35 --- /dev/null +++ b/Userland/Applications/Piano/AudioPlayerLoop.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021, kleines Filmröllchen + * Copyright (c) 2021, JJ Roberts-White + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "AudioPlayerLoop.h" + +#include "TrackManager.h" + +// Converts Piano-internal data to an Audio::Buffer that AudioServer receives +static NonnullRefPtr music_samples_to_buffer(Array samples) +{ + Vector frames; + frames.ensure_capacity(sample_count); + for (auto sample : samples) { + Audio::Frame frame = { sample.left / (double)NumericLimits::max(), sample.right / (double)NumericLimits::max() }; + frames.unchecked_append(frame); + } + return Audio::Buffer::create_with_samples(frames); +} + +AudioPlayerLoop::AudioPlayerLoop(TrackManager& track_manager, bool& need_to_write_wav, Audio::WavWriter& wav_writer) + : m_track_manager(track_manager) + , m_need_to_write_wav(need_to_write_wav) + , m_wav_writer(wav_writer) +{ + m_audio_client = Audio::ClientConnection::construct(); + m_audio_client->on_finish_playing_buffer = [this](int buffer_id) { + (void)buffer_id; + enqueue_audio(); + }; +} + +void AudioPlayerLoop::enqueue_audio() +{ + m_track_manager.fill_buffer(m_buffer); + NonnullRefPtr audio_buffer = music_samples_to_buffer(m_buffer); + m_audio_client->async_enqueue(audio_buffer); + + // FIXME: This should be done somewhere else. + if (m_need_to_write_wav) { + m_need_to_write_wav = false; + m_track_manager.reset(); + m_track_manager.set_should_loop(false); + do { + m_track_manager.fill_buffer(m_buffer); + m_wav_writer.write_samples(reinterpret_cast(m_buffer.data()), buffer_size); + } while (m_track_manager.time()); + m_track_manager.reset(); + m_track_manager.set_should_loop(true); + m_wav_writer.finalize(); + } +} + +void AudioPlayerLoop::toggle_paused() +{ + m_should_play_audio = !m_should_play_audio; + + m_audio_client->set_paused(!m_should_play_audio); +} diff --git a/Userland/Applications/Piano/AudioPlayerLoop.h b/Userland/Applications/Piano/AudioPlayerLoop.h new file mode 100644 index 0000000000..41709745c4 --- /dev/null +++ b/Userland/Applications/Piano/AudioPlayerLoop.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021, kleines Filmröllchen + * Copyright (c) 2021, JJ Roberts-White + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Music.h" +#include +#include +#include + +class TrackManager; + +// Wrapper class accepting custom events to advance the track playing and forward audio data to the system. +// This does not run on a separate thread, preventing IPC multithreading madness. +class AudioPlayerLoop : public Core::Object { + C_OBJECT(AudioPlayerLoop) +public: + AudioPlayerLoop(TrackManager& track_manager, bool& need_to_write_wav, Audio::WavWriter& wav_writer); + + void enqueue_audio(); + + void toggle_paused(); + bool is_playing() { return m_should_play_audio; } + +private: + TrackManager& m_track_manager; + Array m_buffer; + RefPtr m_audio_client; + + bool m_should_play_audio = true; + + bool& m_need_to_write_wav; + Audio::WavWriter& m_wav_writer; +}; diff --git a/Userland/Applications/Piano/CMakeLists.txt b/Userland/Applications/Piano/CMakeLists.txt index 83232f89fb..431219a698 100644 --- a/Userland/Applications/Piano/CMakeLists.txt +++ b/Userland/Applications/Piano/CMakeLists.txt @@ -6,12 +6,14 @@ serenity_component( ) set(SOURCES + AudioPlayerLoop.cpp Track.cpp TrackManager.cpp KeysWidget.cpp KnobsWidget.cpp main.cpp MainWidget.cpp + PlayerWidget.cpp RollWidget.cpp SamplerWidget.cpp WaveWidget.cpp diff --git a/Userland/Applications/Piano/MainWidget.cpp b/Userland/Applications/Piano/MainWidget.cpp index e4b742cbc5..296c0180c0 100644 --- a/Userland/Applications/Piano/MainWidget.cpp +++ b/Userland/Applications/Piano/MainWidget.cpp @@ -8,6 +8,7 @@ #include "MainWidget.h" #include "KeysWidget.h" #include "KnobsWidget.h" +#include "PlayerWidget.h" #include "RollWidget.h" #include "SamplerWidget.h" #include "TrackManager.h" @@ -17,8 +18,9 @@ #include #include -MainWidget::MainWidget(TrackManager& track_manager) +MainWidget::MainWidget(TrackManager& track_manager, AudioPlayerLoop& loop) : m_track_manager(track_manager) + , m_audio_loop(loop) { set_layout(); layout()->set_spacing(2); @@ -35,6 +37,8 @@ MainWidget::MainWidget(TrackManager& track_manager) m_tab_widget->add_tab("Sampler", track_manager); + m_player_widget = add(track_manager, loop); + m_keys_and_knobs_container = add(); m_keys_and_knobs_container->set_layout(); m_keys_and_knobs_container->layout()->set_spacing(2); diff --git a/Userland/Applications/Piano/MainWidget.h b/Userland/Applications/Piano/MainWidget.h index 7bfde18a33..e40c8ff87c 100644 --- a/Userland/Applications/Piano/MainWidget.h +++ b/Userland/Applications/Piano/MainWidget.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2019-2020, William McPherson + * Copyright (c) 2021, JJ Roberts-White * * SPDX-License-Identifier: BSD-2-Clause */ @@ -10,12 +11,14 @@ #include "Music.h" #include +class AudioPlayerLoop; class TrackManager; class WaveWidget; class RollWidget; class SamplerWidget; class KeysWidget; class KnobsWidget; +class PlayerWidget; class MainWidget final : public GUI::Widget { C_OBJECT(MainWidget) @@ -28,7 +31,7 @@ public: void set_octave_and_ensure_note_change(int); private: - explicit MainWidget(TrackManager&); + explicit MainWidget(TrackManager&, AudioPlayerLoop&); virtual void keydown_event(GUI::KeyEvent&) override; virtual void keyup_event(GUI::KeyEvent&) override; @@ -41,6 +44,7 @@ private: void turn_on_pressed_keys(); TrackManager& m_track_manager; + AudioPlayerLoop& m_audio_loop; RefPtr m_wave_widget; RefPtr m_roll_widget; @@ -49,6 +53,7 @@ private: RefPtr m_keys_and_knobs_container; RefPtr m_keys_widget; RefPtr m_knobs_widget; + RefPtr m_player_widget; bool m_keys_pressed[key_code_count] { false }; }; diff --git a/Userland/Applications/Piano/PlayerWidget.cpp b/Userland/Applications/Piano/PlayerWidget.cpp new file mode 100644 index 0000000000..3faab0f7cd --- /dev/null +++ b/Userland/Applications/Piano/PlayerWidget.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021, JJ Roberts-White + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "PlayerWidget.h" + +#include "AudioPlayerLoop.h" +#include "Music.h" +#include "TrackManager.h" +#include +#include + +PlayerWidget::PlayerWidget(TrackManager& manager, AudioPlayerLoop& loop) + : m_track_manager(manager) + , m_audio_loop(loop) +{ + set_layout(); + set_fill_with_background_color(true); + + 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_back_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"); // Go back a note + m_next_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"); // Advance a note + + m_play_button = add(); + m_play_button->set_icon(*m_pause_icon); + m_play_button->set_fixed_width(30); + m_play_button->set_tooltip("Play/Pause playback"); + m_play_button->set_focus_policy(GUI::FocusPolicy::NoFocus); + m_play_button->on_click = [this](unsigned) { + m_audio_loop.toggle_paused(); + + if (m_audio_loop.is_playing()) { + m_play_button->set_icon(*m_pause_icon); + } else { + m_play_button->set_icon(*m_play_icon); + } + }; + + m_back_button = add(); + m_back_button->set_icon(*m_back_icon); + m_back_button->set_fixed_width(30); + m_back_button->set_tooltip("Previous Note"); + m_back_button->set_focus_policy(GUI::FocusPolicy::NoFocus); + m_back_button->on_click = [this](unsigned) { + m_track_manager.time_forward(-(sample_rate / (beats_per_minute / 60) / notes_per_beat)); + }; + + m_next_button = add(); + m_next_button->set_icon(*m_next_icon); + m_next_button->set_fixed_width(30); + m_next_button->set_tooltip("Next Note"); + m_next_button->set_focus_policy(GUI::FocusPolicy::NoFocus); + m_next_button->on_click = [this](unsigned) { + m_track_manager.time_forward((sample_rate / (beats_per_minute / 60) / notes_per_beat)); + }; +} + +PlayerWidget::~PlayerWidget() +{ +} diff --git a/Userland/Applications/Piano/PlayerWidget.h b/Userland/Applications/Piano/PlayerWidget.h new file mode 100644 index 0000000000..3152578b30 --- /dev/null +++ b/Userland/Applications/Piano/PlayerWidget.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021, JJ Roberts-White + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +class AudioPlayerLoop; +class TrackManager; + +class PlayerWidget final : public GUI::Toolbar { + C_OBJECT(PlayerWidget) +public: + virtual ~PlayerWidget() override; + +private: + explicit PlayerWidget(TrackManager&, AudioPlayerLoop&); + + TrackManager& m_track_manager; + AudioPlayerLoop& m_audio_loop; + + RefPtr m_play_icon; + RefPtr m_pause_icon; + RefPtr m_back_icon; + RefPtr m_next_icon; + + RefPtr m_play_button; + RefPtr m_back_button; + RefPtr m_next_button; +}; diff --git a/Userland/Applications/Piano/TrackManager.cpp b/Userland/Applications/Piano/TrackManager.cpp index a21b98dffc..9031a2f45f 100644 --- a/Userland/Applications/Piano/TrackManager.cpp +++ b/Userland/Applications/Piano/TrackManager.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2019-2020, William McPherson + * Copyright (c) 2021 JJ Roberts-White * * SPDX-License-Identifier: BSD-2-Clause */ @@ -16,6 +17,17 @@ TrackManager::~TrackManager() { } +void TrackManager::time_forward(int amount) +{ + int new_value = (static_cast(m_time) + amount) % roll_length; + + if (new_value < 0) { // If the new time value is negaive add roll_length to wrap around + m_time = roll_length + new_value; + } else { + m_time = new_value; + } +} + void TrackManager::fill_buffer(Span buffer) { memset(buffer.data(), 0, buffer_size); diff --git a/Userland/Applications/Piano/TrackManager.h b/Userland/Applications/Piano/TrackManager.h index cd39439657..9986085f63 100644 --- a/Userland/Applications/Piano/TrackManager.h +++ b/Userland/Applications/Piano/TrackManager.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2019-2020, William McPherson + * Copyright (c) 2021, JJ Roberts-White * * SPDX-License-Identifier: BSD-2-Clause */ @@ -26,7 +27,9 @@ public: Span buffer() const { return m_current_front_buffer; } int octave() const { return m_octave; } int octave_base() const { return (m_octave - octave_min) * 12; } + int time() const { return m_time; } + void time_forward(int amount); void fill_buffer(Span); void reset(); diff --git a/Userland/Applications/Piano/main.cpp b/Userland/Applications/Piano/main.cpp index b9e0608f68..671b7a65fb 100644 --- a/Userland/Applications/Piano/main.cpp +++ b/Userland/Applications/Piano/main.cpp @@ -2,10 +2,12 @@ * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2019-2020, William McPherson * Copyright (c) 2021, kleines Filmröllchen + * Copyright (c) 2021, JJ Roberts-White * * SPDX-License-Identifier: BSD-2-Clause */ +#include "AudioPlayerLoop.h" #include "MainWidget.h" #include "TrackManager.h" #include @@ -26,65 +28,6 @@ #include #include -// Converts Piano-internal data to an Audio::Buffer that AudioServer receives -static NonnullRefPtr music_samples_to_buffer(Array samples) -{ - Vector frames; - frames.ensure_capacity(sample_count); - for (auto sample : samples) { - Audio::Frame frame = { sample.left / (double)NumericLimits::max(), sample.right / (double)NumericLimits::max() }; - frames.unchecked_append(frame); - } - return Audio::Buffer::create_with_samples(frames); -} - -// Wrapper class accepting custom events to advance the track playing and forward audio data to the system. -// This does not run on a separate thread, preventing IPC multithreading madness. -class AudioPlayerLoop : public Core::Object { - C_OBJECT(AudioPlayerLoop) -public: - AudioPlayerLoop(TrackManager& track_manager, bool& need_to_write_wav, Audio::WavWriter& wav_writer) - : m_track_manager(track_manager) - , m_need_to_write_wav(need_to_write_wav) - , m_wav_writer(wav_writer) - { - m_audio_client = Audio::ClientConnection::construct(); - m_audio_client->on_finish_playing_buffer = [this](int buffer_id) { - (void)buffer_id; - enqueue_audio(); - }; - } - - void enqueue_audio() - { - m_track_manager.fill_buffer(m_buffer); - NonnullRefPtr audio_buffer = music_samples_to_buffer(m_buffer); - m_audio_client->async_enqueue(audio_buffer); - - // FIXME: This should be done somewhere else. - if (m_need_to_write_wav) { - m_need_to_write_wav = false; - m_track_manager.reset(); - m_track_manager.set_should_loop(false); - do { - m_track_manager.fill_buffer(m_buffer); - m_wav_writer.write_samples(reinterpret_cast(m_buffer.data()), buffer_size); - } while (m_track_manager.time()); - m_track_manager.reset(); - m_track_manager.set_should_loop(true); - m_wav_writer.finalize(); - } - } - -private: - TrackManager& m_track_manager; - Array m_buffer; - RefPtr m_audio_client; - - bool& m_need_to_write_wav; - Audio::WavWriter& m_wav_writer; -}; - int main(int argc, char** argv) { if (pledge("stdio thread rpath cpath wpath recvfd sendfd unix", nullptr) < 0) { @@ -96,14 +39,6 @@ int main(int argc, char** argv) TrackManager track_manager; - auto app_icon = GUI::Icon::default_icon("app-piano"); - auto window = GUI::Window::construct(); - auto& main_widget = window->set_main_widget(track_manager); - window->set_title("Piano"); - window->resize(840, 600); - window->set_icon(app_icon.bitmap_for_size(16)); - window->show(); - Audio::WavWriter wav_writer; Optional save_path; bool need_to_write_wav = false; @@ -112,6 +47,14 @@ int main(int argc, char** argv) audio_loop->enqueue_audio(); audio_loop->enqueue_audio(); + auto app_icon = GUI::Icon::default_icon("app-piano"); + auto window = GUI::Window::construct(); + auto& main_widget = window->set_main_widget(track_manager, audio_loop); + window->set_title("Piano"); + window->resize(840, 600); + window->set_icon(app_icon.bitmap_for_size(16)); + window->show(); + auto main_widget_updater = Core::Timer::construct(static_cast((1 / 60.0) * 1000), [&] { Core::EventLoop::current().post_event(main_widget, make(0)); });