diff --git a/Applications/Piano/AudioEngine.cpp b/Applications/Piano/AudioEngine.cpp index c3ada8eb28ac4d..8bfd4dcdf6569d 100644 --- a/Applications/Piano/AudioEngine.cpp +++ b/Applications/Piano/AudioEngine.cpp @@ -26,6 +26,7 @@ */ #include "AudioEngine.h" +#include #include #include @@ -94,6 +95,9 @@ void AudioEngine::fill_buffer(FixedArray& buffer) case Wave::Noise: val = (volume * m_power[note]) * noise(); break; + case Wave::RecordedSample: + val = (volume * m_power[note]) * recorded_sample(note); + break; default: ASSERT_NOT_REACHED(); } @@ -143,6 +147,37 @@ void AudioEngine::reset() m_previous_column = horizontal_notes - 1; } +String AudioEngine::set_recorded_sample(const StringView& path) +{ + Audio::WavLoader wav_loader(path); + if (wav_loader.has_error()) + return String(wav_loader.error_string()); + auto wav_buffer = wav_loader.get_more_samples(60 * sample_rate * sizeof(Sample)); // 1 minute maximum + + if (!m_recorded_sample.is_empty()) + m_recorded_sample.clear(); + m_recorded_sample.resize(wav_buffer->sample_count()); + + float peak = 0; + for (int i = 0; i < wav_buffer->sample_count(); ++i) { + float left_abs = fabs(wav_buffer->samples()[i].left); + float right_abs = fabs(wav_buffer->samples()[i].right); + if (left_abs > peak) + peak = left_abs; + if (right_abs > peak) + peak = right_abs; + } + + if (peak) { + for (int i = 0; i < wav_buffer->sample_count(); ++i) { + m_recorded_sample[i].left = wav_buffer->samples()[i].left / peak; + m_recorded_sample[i].right = wav_buffer->samples()[i].right / peak; + } + } + + return String::empty(); +} + // All of the information for these waves is on Wikipedia. double AudioEngine::sine(size_t note) @@ -188,6 +223,21 @@ double AudioEngine::noise() const return w; } +double AudioEngine::recorded_sample(size_t note) +{ + int t = m_pos[note]; + if (t >= m_recorded_sample.size()) + return 0; + double w = m_recorded_sample[t].left; + if (t + 1 < m_recorded_sample.size()) { + double t_fraction = m_pos[note] - t; + w += (m_recorded_sample[t + 1].left - m_recorded_sample[t].left) * t_fraction; + } + double recorded_sample_step = note_frequencies[note] / middle_c; + m_pos[note] += recorded_sample_step; + return w; +} + static inline double calculate_step(double distance, int milliseconds) { if (milliseconds == 0) diff --git a/Applications/Piano/AudioEngine.h b/Applications/Piano/AudioEngine.h index 72d3e17a133f85..4fa69ff449e9cb 100644 --- a/Applications/Piano/AudioEngine.h +++ b/Applications/Piano/AudioEngine.h @@ -31,6 +31,7 @@ #include #include #include +#include class AudioEngine { AK_MAKE_NONCOPYABLE(AudioEngine) @@ -40,6 +41,7 @@ class AudioEngine { ~AudioEngine(); const FixedArray& buffer() const { return *m_front_buffer_ptr; } + const Vector& recorded_sample() const { return m_recorded_sample; } void reset(); Switch roll_note(int y, int x) const { return m_roll_notes[y][x]; } int current_column() const { return m_current_column; } @@ -55,6 +57,7 @@ class AudioEngine { int tick() const { return m_tick; } void fill_buffer(FixedArray& buffer); + String set_recorded_sample(const StringView& path); void set_note(int note, Switch); void set_note_current_octave(int note, Switch); void set_roll_note(int y, int x, Switch); @@ -73,6 +76,7 @@ class AudioEngine { double square(size_t note); double triangle(size_t note); double noise() const; + double recorded_sample(size_t note); void update_roll(); void set_notes_from_roll(); @@ -86,6 +90,8 @@ class AudioEngine { Queue>> m_delay_buffers; + Vector m_recorded_sample; + u8 m_note_on[note_count] { 0 }; double m_power[note_count] { 0 }; double m_pos[note_count]; // Initialized lazily. diff --git a/Applications/Piano/MainWidget.cpp b/Applications/Piano/MainWidget.cpp index c1db1e21a38a02..a119769f783843 100644 --- a/Applications/Piano/MainWidget.cpp +++ b/Applications/Piano/MainWidget.cpp @@ -30,8 +30,10 @@ #include "KeysWidget.h" #include "KnobsWidget.h" #include "RollWidget.h" +#include "SamplerWidget.h" #include "WaveWidget.h" #include +#include MainWidget::MainWidget(AudioEngine& audio_engine) : m_audio_engine(audio_engine) @@ -45,10 +47,16 @@ MainWidget::MainWidget(AudioEngine& audio_engine) m_wave_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); m_wave_widget->set_preferred_size(0, 100); - m_roll_widget = RollWidget::construct(this, audio_engine); + m_roll_widget = RollWidget::construct(nullptr, audio_engine); m_roll_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fill); m_roll_widget->set_preferred_size(0, 300); + m_sampler_widget = SamplerWidget::construct(nullptr, audio_engine); + + m_tab_widget = GUI::TabWidget::construct(this); + m_tab_widget->add_widget("Piano Roll", m_roll_widget); + m_tab_widget->add_widget("Sampler", m_sampler_widget); + m_keys_and_knobs_container = GUI::Widget::construct(this); m_keys_and_knobs_container->set_layout(make()); m_keys_and_knobs_container->layout()->set_spacing(2); diff --git a/Applications/Piano/MainWidget.h b/Applications/Piano/MainWidget.h index d16e649330d320..ebd761ea928f2c 100644 --- a/Applications/Piano/MainWidget.h +++ b/Applications/Piano/MainWidget.h @@ -33,9 +33,14 @@ class AudioEngine; class WaveWidget; class RollWidget; +class SamplerWidget; class KeysWidget; class KnobsWidget; +namespace GUI { +class TabWidget; +} + class MainWidget final : public GUI::Widget { C_OBJECT(MainWidget) public: @@ -57,6 +62,8 @@ class MainWidget final : public GUI::Widget { RefPtr m_wave_widget; RefPtr m_roll_widget; + RefPtr m_sampler_widget; + RefPtr m_tab_widget; RefPtr m_keys_and_knobs_container; RefPtr m_keys_widget; RefPtr m_knobs_widget; diff --git a/Applications/Piano/Makefile b/Applications/Piano/Makefile index 62f3be225d0b65..aadbd615deaef0 100644 --- a/Applications/Piano/Makefile +++ b/Applications/Piano/Makefile @@ -3,6 +3,7 @@ OBJS = \ MainWidget.o \ WaveWidget.o \ RollWidget.o \ + SamplerWidget.o \ KeysWidget.o \ KnobsWidget.o \ main.o diff --git a/Applications/Piano/Music.h b/Applications/Piano/Music.h index 2a0a030cf85fdc..ddcc2fb2aee8fd 100644 --- a/Applications/Piano/Music.h +++ b/Applications/Piano/Music.h @@ -67,6 +67,7 @@ enum Wave { Square, Saw, Noise, + RecordedSample, }; constexpr const char* wave_strings[] = { @@ -75,10 +76,11 @@ constexpr const char* wave_strings[] = { "Square", "Saw", "Noise", + "Sample", }; constexpr int first_wave = Sine; -constexpr int last_wave = Noise; +constexpr int last_wave = RecordedSample; enum Envelope { Done, @@ -110,6 +112,45 @@ constexpr KeyColor key_pattern[] = { const Color note_pressed_color(64, 64, 255); const Color column_playing_color(128, 128, 255); +const Color wave_colors[] = { + // Sine + { + 255, + 192, + 0, + }, + // Triangle + { + 35, + 171, + 35, + }, + // Square + { + 128, + 160, + 255, + }, + // Saw + { + 240, + 100, + 128, + }, + // Noise + { + 197, + 214, + 225, + }, + // RecordedSample + { + 227, + 39, + 39, + }, +}; + constexpr int notes_per_octave = 12; constexpr int white_keys_per_octave = 7; constexpr int black_keys_per_octave = 5; @@ -217,6 +258,8 @@ constexpr double note_frequencies[] = { }; constexpr int note_count = sizeof(note_frequencies) / sizeof(double); +constexpr double middle_c = note_frequencies[36]; + } using namespace Music; diff --git a/Applications/Piano/SamplerWidget.cpp b/Applications/Piano/SamplerWidget.cpp new file mode 100644 index 00000000000000..78eb08eee998c6 --- /dev/null +++ b/Applications/Piano/SamplerWidget.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020-2020, William McPherson + * 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 "SamplerWidget.h" +#include "AudioEngine.h" +#include +#include +#include +#include +#include +#include + +WaveEditor::WaveEditor(GUI::Widget* parent, AudioEngine& audio_engine) + : GUI::Frame(parent) + , m_audio_engine(audio_engine) +{ + set_frame_thickness(2); + set_frame_shadow(Gfx::FrameShadow::Sunken); + set_frame_shape(Gfx::FrameShape::Container); +} + +WaveEditor::~WaveEditor() +{ +} + +int WaveEditor::sample_to_y(float percentage) const +{ + double portion_of_half_height = percentage * ((frame_inner_rect().height() - 1) / 2.0); + double y = (frame_inner_rect().height() / 2.0) + portion_of_half_height; + return y; +} + +void WaveEditor::paint_event(GUI::PaintEvent& event) +{ + GUI::Frame::paint_event(event); + + GUI::Painter painter(*this); + painter.fill_rect(frame_inner_rect(), Color::Black); + + auto recorded_sample = m_audio_engine.recorded_sample(); + if (recorded_sample.is_empty()) + return; + + double width_scale = static_cast(frame_inner_rect().width()) / recorded_sample.size(); + + painter.translate(frame_thickness(), frame_thickness()); + + int prev_x = 0; + int prev_y = sample_to_y(recorded_sample[0].left); + painter.set_pixel({ prev_x, prev_y }, wave_colors[RecordedSample]); + + for (int x = 1; x < recorded_sample.size(); ++x) { + int y = sample_to_y(recorded_sample[x].left); + + Gfx::Point point1(prev_x * width_scale, prev_y); + Gfx::Point point2(x * width_scale, y); + painter.draw_line(point1, point2, wave_colors[RecordedSample]); + + prev_x = x; + prev_y = y; + } +} + +SamplerWidget::SamplerWidget(GUI::Widget* parent, AudioEngine& audio_engine) + : GUI::Frame(parent) + , m_audio_engine(audio_engine) +{ + set_frame_thickness(2); + set_frame_shadow(Gfx::FrameShadow::Sunken); + set_frame_shape(Gfx::FrameShape::Container); + set_layout(make()); + layout()->set_margins({ 10, 10, 10, 10 }); + layout()->set_spacing(10); + set_fill_with_background_color(true); + + m_open_button_and_recorded_sample_name_container = GUI::Widget::construct(this); + m_open_button_and_recorded_sample_name_container->set_layout(make()); + m_open_button_and_recorded_sample_name_container->layout()->set_spacing(10); + m_open_button_and_recorded_sample_name_container->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + m_open_button_and_recorded_sample_name_container->set_preferred_size(0, 24); + + m_open_button = GUI::Button::construct(m_open_button_and_recorded_sample_name_container); + m_open_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + m_open_button->set_preferred_size(24, 24); + m_open_button->set_focusable(false); + m_open_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png")); + m_open_button->on_click = [this](const auto&) { + Optional open_path = GUI::FilePicker::get_open_filepath(); + if (!open_path.has_value()) + return; + String error_string = m_audio_engine.set_recorded_sample(open_path.value()); + if (!error_string.is_empty()) { + GUI::MessageBox::show(String::format("Failed to load WAV file: %s", error_string.characters()), "Error", GUI::MessageBox::Type::Error); + return; + } + m_recorded_sample_name->set_text(open_path.value()); + m_wave_editor->update(); + }; + + m_recorded_sample_name = GUI::Label::construct("No sample loaded", m_open_button_and_recorded_sample_name_container); + m_recorded_sample_name->set_text_alignment(Gfx::TextAlignment::CenterLeft); + + m_wave_editor = WaveEditor::construct(this, m_audio_engine); + m_wave_editor->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + m_wave_editor->set_preferred_size(0, 100); +} + +SamplerWidget::~SamplerWidget() +{ +} diff --git a/Applications/Piano/SamplerWidget.h b/Applications/Piano/SamplerWidget.h new file mode 100644 index 00000000000000..8d70a43816bcee --- /dev/null +++ b/Applications/Piano/SamplerWidget.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020-2020, William McPherson + * 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 + +namespace GUI { +class Label; +class Button; +} + +class AudioEngine; + +class WaveEditor final : public GUI::Frame { + C_OBJECT(WaveEditor) +public: + virtual ~WaveEditor() override; + +private: + WaveEditor(GUI::Widget* parent, AudioEngine&); + + virtual void paint_event(GUI::PaintEvent&) override; + + int sample_to_y(float percentage) const; + + AudioEngine& m_audio_engine; +}; + +class SamplerWidget final : public GUI::Frame { + C_OBJECT(SamplerWidget) +public: + virtual ~SamplerWidget() override; + +private: + SamplerWidget(GUI::Widget* parent, AudioEngine&); + + AudioEngine& m_audio_engine; + + RefPtr m_open_button_and_recorded_sample_name_container; + RefPtr m_open_button; + RefPtr m_recorded_sample_name; + RefPtr m_wave_editor; +}; diff --git a/Applications/Piano/WaveWidget.cpp b/Applications/Piano/WaveWidget.cpp index 483ae2e1c1e585..bd2430099ee0ef 100644 --- a/Applications/Piano/WaveWidget.cpp +++ b/Applications/Piano/WaveWidget.cpp @@ -43,39 +43,6 @@ WaveWidget::~WaveWidget() { } -static const Color wave_colors[] = { - // Sine - { - 255, - 192, - 0, - }, - // Triangle - { - 35, - 171, - 35, - }, - // Square - { - 128, - 160, - 255, - }, - // Saw - { - 240, - 100, - 128, - }, - // Noise - { - 197, - 214, - 225, - }, -}; - int WaveWidget::sample_to_y(int sample) const { constexpr double sample_max = std::numeric_limits::max();