summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Applications/Piano/AudioEngine.cpp50
-rw-r--r--Applications/Piano/AudioEngine.h6
-rw-r--r--Applications/Piano/MainWidget.cpp10
-rw-r--r--Applications/Piano/MainWidget.h7
-rw-r--r--Applications/Piano/Makefile1
-rw-r--r--Applications/Piano/Music.h45
-rw-r--r--Applications/Piano/SamplerWidget.cpp133
-rw-r--r--Applications/Piano/SamplerWidget.h67
-rw-r--r--Applications/Piano/WaveWidget.cpp33
9 files changed, 317 insertions, 35 deletions
diff --git a/Applications/Piano/AudioEngine.cpp b/Applications/Piano/AudioEngine.cpp
index c3ada8eb28..8bfd4dcdf6 100644
--- a/Applications/Piano/AudioEngine.cpp
+++ b/Applications/Piano/AudioEngine.cpp
@@ -26,6 +26,7 @@
*/
#include "AudioEngine.h"
+#include <LibAudio/WavLoader.h>
#include <limits>
#include <math.h>
@@ -94,6 +95,9 @@ void AudioEngine::fill_buffer(FixedArray<Sample>& 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 72d3e17a13..4fa69ff449 100644
--- a/Applications/Piano/AudioEngine.h
+++ b/Applications/Piano/AudioEngine.h
@@ -31,6 +31,7 @@
#include <AK/FixedArray.h>
#include <AK/Noncopyable.h>
#include <AK/Queue.h>
+#include <LibAudio/Buffer.h>
class AudioEngine {
AK_MAKE_NONCOPYABLE(AudioEngine)
@@ -40,6 +41,7 @@ public:
~AudioEngine();
const FixedArray<Sample>& buffer() const { return *m_front_buffer_ptr; }
+ const Vector<Audio::Sample>& 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 @@ public:
int tick() const { return m_tick; }
void fill_buffer(FixedArray<Sample>& 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 @@ private:
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 @@ private:
Queue<NonnullOwnPtr<FixedArray<Sample>>> m_delay_buffers;
+ Vector<Audio::Sample> 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 c1db1e21a3..a119769f78 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 <LibGUI/BoxLayout.h>
+#include <LibGUI/TabWidget.h>
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<GUI::HorizontalBoxLayout>());
m_keys_and_knobs_container->layout()->set_spacing(2);
diff --git a/Applications/Piano/MainWidget.h b/Applications/Piano/MainWidget.h
index d16e649330..ebd761ea92 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 @@ private:
RefPtr<WaveWidget> m_wave_widget;
RefPtr<RollWidget> m_roll_widget;
+ RefPtr<SamplerWidget> m_sampler_widget;
+ RefPtr<GUI::TabWidget> m_tab_widget;
RefPtr<GUI::Widget> m_keys_and_knobs_container;
RefPtr<KeysWidget> m_keys_widget;
RefPtr<KnobsWidget> m_knobs_widget;
diff --git a/Applications/Piano/Makefile b/Applications/Piano/Makefile
index 62f3be225d..aadbd615de 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 2a0a030cf8..ddcc2fb2ae 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 0000000000..78eb08eee9
--- /dev/null
+++ b/Applications/Piano/SamplerWidget.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2020-2020, William McPherson <willmcpherson2@gmail.com>
+ * 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 <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+
+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<double>(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<GUI::VerticalBoxLayout>());
+ 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<GUI::HorizontalBoxLayout>());
+ 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<String> 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 0000000000..8d70a43816
--- /dev/null
+++ b/Applications/Piano/SamplerWidget.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2020-2020, William McPherson <willmcpherson2@gmail.com>
+ * 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 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<GUI::Widget> m_open_button_and_recorded_sample_name_container;
+ RefPtr<GUI::Button> m_open_button;
+ RefPtr<GUI::Label> m_recorded_sample_name;
+ RefPtr<WaveEditor> m_wave_editor;
+};
diff --git a/Applications/Piano/WaveWidget.cpp b/Applications/Piano/WaveWidget.cpp
index 483ae2e1c1..bd2430099e 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<i16>::max();