Browse Source

Piano: Rewrite application

Goals:
- Switch to a more typical LibGUI arrangement
- Separate GUI (MainWidget) and audio (AudioEngine)
- Improve on existing features while retaining the same feature set

Improvements:
- Each GUI element is a separate widget
- The wave (WaveWidget) scales with the window
- The piano roll (RollWidget) scales horizontally and scrolls vertically
- The piano (KeysWidget) fits as many notes as possible
- The knobs (KnobsWidget) are now sliders
- All mouse and key events are handled in constant time
- The octave can be changed while playing notes
- The same note can be played with the mouse, keyboard and roll at the
  same time, and the volume of the resulting note is scaled accordingly
- Note frequency constants use the maximum precision available in a
  double
William McPherson 5 years ago
parent
commit
4a36a51618

+ 216 - 0
Applications/Piano/AudioEngine.cpp

@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "AudioEngine.h"
+#include <limits>
+#include <math.h>
+
+AudioEngine::AudioEngine()
+{
+}
+
+AudioEngine::~AudioEngine()
+{
+}
+
+void AudioEngine::fill_buffer(FixedArray<Sample>& buffer)
+{
+    memset(buffer.data(), 0, buffer_size);
+
+    for (size_t i = 0; i < buffer.size(); ++i) {
+        for (size_t note = 0; note < note_count; ++note) {
+            if (!m_note_on[note])
+                continue;
+            double val = 0;
+            switch (m_wave) {
+            case Wave::Sine:
+                val = (volume * m_power[note]) * sine(note);
+                break;
+            case Wave::Saw:
+                val = (volume * m_power[note]) * saw(note);
+                break;
+            case Wave::Square:
+                val = (volume * m_power[note]) * square(note);
+                break;
+            case Wave::Triangle:
+                val = (volume * m_power[note]) * triangle(note);
+                break;
+            case Wave::Noise:
+                val = (volume * m_power[note]) * noise();
+                break;
+            default:
+                ASSERT_NOT_REACHED();
+            }
+            buffer[i].left += val;
+        }
+        buffer[i].right = buffer[i].left;
+    }
+
+    if (m_decay) {
+        for (size_t note = 0; note < note_count; ++note) {
+            if (m_note_on[note]) {
+                m_power[note] -= m_decay / 100.0;
+                if (m_power[note] < 0)
+                    m_power[note] = 0;
+            }
+        }
+    }
+
+    if (m_delay) {
+        if (m_delay_buffers.size() >= m_delay) {
+            auto to_blend = m_delay_buffers.dequeue();
+            for (size_t i = 0; i < to_blend->size(); ++i) {
+                buffer[i].left += (*to_blend)[i].left * 0.333333;
+                buffer[i].right += (*to_blend)[i].right * 0.333333;
+            }
+        }
+
+        auto delay_buffer = make<FixedArray<Sample>>(buffer.size());
+        memcpy(delay_buffer->data(), buffer.data(), buffer_size);
+        m_delay_buffers.enqueue(move(delay_buffer));
+    }
+
+    if (++m_time == m_tick)
+        m_time = 0;
+
+    memcpy(m_back_buffer_ptr->data(), buffer.data(), buffer_size);
+    swap(m_front_buffer_ptr, m_back_buffer_ptr);
+}
+
+// All of the information for these waves is on Wikipedia.
+
+double AudioEngine::sine(size_t note)
+{
+    double pos = note_frequencies[note] / sample_rate;
+    double sin_step = pos * 2 * M_PI;
+    double w = sin(m_pos[note]);
+    m_pos[note] += sin_step;
+    return w;
+}
+
+double AudioEngine::saw(size_t note)
+{
+    double saw_step = note_frequencies[note] / sample_rate;
+    double t = m_pos[note];
+    double w = (0.5 - (t - floor(t))) * 2;
+    m_pos[note] += saw_step;
+    return w;
+}
+
+double AudioEngine::square(size_t note)
+{
+    double pos = note_frequencies[note] / sample_rate;
+    double square_step = pos * 2 * M_PI;
+    double w = sin(m_pos[note]) >= 0 ? 1 : -1;
+    m_pos[note] += square_step;
+    return w;
+}
+
+double AudioEngine::triangle(size_t note)
+{
+    double triangle_step = note_frequencies[note] / sample_rate;
+    double t = m_pos[note];
+    double w = fabs(fmod((4 * t) + 1, 4) - 2) - 1;
+    m_pos[note] += triangle_step;
+    return w;
+}
+
+double AudioEngine::noise() const
+{
+    double random_percentage = static_cast<double>(rand()) / RAND_MAX;
+    double w = (random_percentage * 2) - 1;
+    return w;
+}
+
+void AudioEngine::set_note(int note, Switch switch_note)
+{
+    ASSERT(note >= 0 && note < note_count);
+
+    if (switch_note == On) {
+        if (m_note_on[note] == 0) {
+            m_pos[note] = 0;
+            m_power[note] = 0;
+        }
+        ++m_power[note];
+        ++m_note_on[note];
+    } else {
+        if (m_note_on[note] >= 1) {
+            if (--m_power[note] < 0)
+                m_power[note] = 0;
+            --m_note_on[note];
+        }
+    }
+
+    ASSERT(m_note_on[note] != std::numeric_limits<u8>::max());
+    ASSERT(m_power[note] >= 0);
+}
+
+void AudioEngine::set_note_current_octave(int note, Switch switch_note)
+{
+    set_note(note + octave_base(), switch_note);
+}
+
+void AudioEngine::set_octave(Direction direction)
+{
+    if (direction == Up) {
+        if (m_octave < octave_max)
+            ++m_octave;
+    } else {
+        if (m_octave > octave_min)
+            --m_octave;
+    }
+}
+
+void AudioEngine::set_wave(int wave)
+{
+    ASSERT(wave >= first_wave && wave <= last_wave);
+    m_wave = wave;
+}
+
+void AudioEngine::set_wave(Direction direction)
+{
+    if (direction == Up) {
+        if (++m_wave > last_wave)
+            m_wave = first_wave;
+    } else {
+        if (--m_wave < first_wave)
+            m_wave = last_wave;
+    }
+}
+
+void AudioEngine::set_decay(int decay)
+{
+    ASSERT(decay >= 0);
+    m_decay = decay;
+}
+
+void AudioEngine::set_delay(int delay)
+{
+    ASSERT(delay >= 0);
+    m_delay_buffers.clear();
+    m_delay = delay;
+}

+ 85 - 0
Applications/Piano/AudioEngine.h

@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "Music.h"
+#include <AK/FixedArray.h>
+#include <AK/Noncopyable.h>
+#include <AK/Queue.h>
+
+class AudioEngine {
+    AK_MAKE_NONCOPYABLE(AudioEngine)
+    AK_MAKE_NONMOVABLE(AudioEngine)
+public:
+    AudioEngine();
+    ~AudioEngine();
+
+    const FixedArray<Sample>& buffer() const { return *m_front_buffer_ptr; }
+    int octave() const { return m_octave; }
+    int octave_base() const { return (m_octave - octave_min) * 12; }
+    int wave() const { return m_wave; }
+    int decay() const { return m_decay; }
+    int delay() const { return m_delay; }
+    int time() const { return m_time; }
+    int tick() const { return m_tick; }
+
+    void fill_buffer(FixedArray<Sample>& buffer);
+    void set_note(int note, Switch);
+    void set_note_current_octave(int note, Switch);
+    void set_octave(Direction);
+    void set_wave(int wave);
+    void set_wave(Direction);
+    void set_decay(int decay);
+    void set_delay(int delay);
+
+private:
+    double sine(size_t note);
+    double saw(size_t note);
+    double square(size_t note);
+    double triangle(size_t note);
+    double noise() const;
+
+    FixedArray<Sample> m_front_buffer { sample_count };
+    FixedArray<Sample> m_back_buffer { sample_count };
+    FixedArray<Sample>* m_front_buffer_ptr { &m_front_buffer };
+    FixedArray<Sample>* m_back_buffer_ptr { &m_back_buffer };
+
+    Queue<NonnullOwnPtr<FixedArray<Sample>>> m_delay_buffers;
+
+    u8 m_note_on[note_count] { 0 };
+    double m_power[note_count]; // Initialized lazily.
+    double m_pos[note_count];   // Initialized lazily.
+
+    int m_octave { 4 };
+    int m_wave { first_wave };
+    int m_decay { 0 };
+    int m_delay { 0 };
+
+    int m_time { 0 };
+    int m_tick { 8 };
+};

+ 320 - 0
Applications/Piano/KeysWidget.cpp

@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "KeysWidget.h"
+#include "AudioEngine.h"
+#include <LibGUI/GPainter.h>
+
+KeysWidget::KeysWidget(GWidget* parent, AudioEngine& audio_engine)
+    : GFrame(parent)
+    , m_audio_engine(audio_engine)
+{
+    set_frame_thickness(2);
+    set_frame_shadow(FrameShadow::Sunken);
+    set_frame_shape(FrameShape::Container);
+    set_fill_with_background_color(true);
+}
+
+KeysWidget::~KeysWidget()
+{
+}
+
+int KeysWidget::mouse_note() const
+{
+    if (m_mouse_down && m_mouse_note + m_audio_engine.octave_base() < note_count)
+        return m_mouse_note; // Can be -1.
+    else
+        return -1;
+}
+
+void KeysWidget::set_key(int key, Switch switch_key)
+{
+    if (key == -1 || key + m_audio_engine.octave_base() >= note_count)
+        return;
+
+    if (switch_key == On) {
+        ++m_key_on[key];
+    } else {
+        if (m_key_on[key] >= 1)
+            --m_key_on[key];
+    }
+    ASSERT(m_key_on[key] <= 2);
+
+    m_audio_engine.set_note_current_octave(key, switch_key);
+}
+
+int KeysWidget::key_code_to_key(int key_code) const
+{
+    switch (key_code) {
+    case Key_A:
+        return 0;
+    case Key_W:
+        return 1;
+    case Key_S:
+        return 2;
+    case Key_E:
+        return 3;
+    case Key_D:
+        return 4;
+    case Key_F:
+        return 5;
+    case Key_T:
+        return 6;
+    case Key_G:
+        return 7;
+    case Key_Y:
+        return 8;
+    case Key_H:
+        return 9;
+    case Key_U:
+        return 10;
+    case Key_J:
+        return 11;
+    case Key_K:
+        return 12;
+    case Key_O:
+        return 13;
+    case Key_L:
+        return 14;
+    case Key_P:
+        return 15;
+    case Key_Semicolon:
+        return 16;
+    case Key_Apostrophe:
+        return 17;
+    case Key_RightBracket:
+        return 18;
+    case Key_Return:
+        return 19;
+    default:
+        return -1;
+    }
+}
+
+constexpr int white_key_width = 24;
+constexpr int black_key_width = 16;
+constexpr int black_key_x_offset = black_key_width / 2;
+constexpr int black_key_height = 60;
+
+constexpr char white_key_labels[] = {
+    'A',
+    'S',
+    'D',
+    'F',
+    'G',
+    'H',
+    'J',
+    'K',
+    'L',
+    ';',
+    '\'',
+    'r',
+};
+constexpr int white_key_labels_count = sizeof(white_key_labels) / sizeof(char);
+
+constexpr char black_key_labels[] = {
+    'W',
+    'E',
+    'T',
+    'Y',
+    'U',
+    'O',
+    'P',
+    ']',
+};
+constexpr int black_key_labels_count = sizeof(black_key_labels) / sizeof(char);
+
+constexpr int black_key_offsets[] = {
+    white_key_width,
+    white_key_width * 2,
+    white_key_width,
+    white_key_width,
+    white_key_width * 2,
+};
+
+constexpr int white_key_note_accumulator[] = {
+    2,
+    2,
+    1,
+    2,
+    2,
+    2,
+    1,
+};
+
+constexpr int black_key_note_accumulator[] = {
+    2,
+    3,
+    2,
+    2,
+    3,
+};
+
+void KeysWidget::paint_event(GPaintEvent& event)
+{
+    GPainter painter(*this);
+    painter.translate(frame_thickness(), frame_thickness());
+
+    int note = 0;
+    int x = 0;
+    int i = 0;
+    for (;;) {
+        Rect rect(x, 0, white_key_width, frame_inner_rect().height());
+        painter.fill_rect(rect, m_key_on[note] ? note_pressed_color : Color::White);
+        painter.draw_rect(rect, Color::Black);
+        if (i < white_key_labels_count) {
+            rect.set_height(rect.height() * 1.5);
+            painter.draw_text(rect, StringView(&white_key_labels[i], 1), TextAlignment::Center, Color::Black);
+        }
+
+        note += white_key_note_accumulator[i % white_keys_per_octave];
+        x += white_key_width;
+        ++i;
+
+        if (note + m_audio_engine.octave_base() >= note_count)
+            break;
+        if (x >= frame_inner_rect().width())
+            break;
+    }
+
+    note = 1;
+    x = white_key_width - black_key_x_offset;
+    i = 0;
+    for (;;) {
+        Rect rect(x, 0, black_key_width, black_key_height);
+        painter.fill_rect(rect, m_key_on[note] ? note_pressed_color : Color::Black);
+        painter.draw_rect(rect, Color::Black);
+        if (i < black_key_labels_count) {
+            rect.set_height(rect.height() * 1.5);
+            painter.draw_text(rect, StringView(&black_key_labels[i], 1), TextAlignment::Center, Color::White);
+        }
+
+        note += black_key_note_accumulator[i % black_keys_per_octave];
+        x += black_key_offsets[i % black_keys_per_octave];
+        ++i;
+
+        if (note + m_audio_engine.octave_base() >= note_count)
+            break;
+        if (x >= frame_inner_rect().width())
+            break;
+    }
+
+    GFrame::paint_event(event);
+}
+
+constexpr int notes_per_white_key[] = {
+    1,
+    3,
+    5,
+    6,
+    8,
+    10,
+    12,
+};
+
+// Keep in mind that in any of these functions a note value can be out of
+// bounds. Bounds checking is done in set_key().
+
+static inline int note_from_white_keys(int white_keys)
+{
+    int octaves = white_keys / white_keys_per_octave;
+    int remainder = white_keys % white_keys_per_octave;
+    int notes_from_octaves = octaves * notes_per_octave;
+    int notes_from_remainder = notes_per_white_key[remainder];
+    int note = (notes_from_octaves + notes_from_remainder) - 1;
+    return note;
+}
+
+int KeysWidget::note_for_event_position(Point point) const
+{
+    if (!frame_inner_rect().contains(point))
+        return -1;
+
+    point.move_by(-frame_thickness(), -frame_thickness());
+
+    int white_keys = point.x() / white_key_width;
+    int note = note_from_white_keys(white_keys);
+
+    bool black_key_on_left = note != 0 && key_pattern[(note - 1) % notes_per_octave] == Black;
+    if (black_key_on_left) {
+        int black_key_x = (white_keys * white_key_width) - black_key_x_offset;
+        Rect black_key(black_key_x, 0, black_key_width, black_key_height);
+        if (black_key.contains(point))
+            return note - 1;
+    }
+
+    bool black_key_on_right = key_pattern[(note + 1) % notes_per_octave] == Black;
+    if (black_key_on_right) {
+        int black_key_x = ((white_keys + 1) * white_key_width) - black_key_x_offset;
+        Rect black_key(black_key_x, 0, black_key_width, black_key_height);
+        if (black_key.contains(point))
+            return note + 1;
+    }
+
+    return note;
+}
+
+void KeysWidget::mousedown_event(GMouseEvent& event)
+{
+    if (event.button() != Left)
+        return;
+
+    m_mouse_down = true;
+
+    m_mouse_note = note_for_event_position(event.position());
+
+    set_key(m_mouse_note, On);
+    update();
+}
+
+void KeysWidget::mouseup_event(GMouseEvent& event)
+{
+    if (event.button() != Left)
+        return;
+
+    m_mouse_down = false;
+
+    set_key(m_mouse_note, Off);
+    update();
+}
+
+void KeysWidget::mousemove_event(GMouseEvent& event)
+{
+    if (!m_mouse_down)
+        return;
+
+    int new_mouse_note = note_for_event_position(event.position());
+
+    if (m_mouse_note == new_mouse_note)
+        return;
+
+    set_key(m_mouse_note, Off);
+    set_key(new_mouse_note, On);
+    update();
+
+    m_mouse_note = new_mouse_note;
+}

+ 61 - 0
Applications/Piano/KeysWidget.h

@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "Music.h"
+#include <LibGUI/GFrame.h>
+
+class AudioEngine;
+
+class KeysWidget final : public GFrame {
+    C_OBJECT(KeysWidget)
+public:
+    virtual ~KeysWidget() override;
+
+    int key_code_to_key(int key_code) const;
+    int mouse_note() const;
+
+    void set_key(int key, Switch);
+
+private:
+    KeysWidget(GWidget* parent, AudioEngine&);
+
+    virtual void paint_event(GPaintEvent&) override;
+    virtual void mousedown_event(GMouseEvent&) override;
+    virtual void mouseup_event(GMouseEvent&) override;
+    virtual void mousemove_event(GMouseEvent&) override;
+
+    int note_for_event_position(Point) const;
+
+    AudioEngine& m_audio_engine;
+
+    u8 m_key_on[note_count] { 0 };
+
+    bool m_mouse_down { false };
+    int m_mouse_note { -1 };
+};

+ 131 - 0
Applications/Piano/KnobsWidget.cpp

@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "KnobsWidget.h"
+#include "AudioEngine.h"
+#include "MainWidget.h"
+#include <LibGUI/GBoxLayout.h>
+#include <LibGUI/GLabel.h>
+#include <LibGUI/GSlider.h>
+
+KnobsWidget::KnobsWidget(GWidget* parent, AudioEngine& audio_engine, MainWidget& main_widget)
+    : GFrame(parent)
+    , m_audio_engine(audio_engine)
+    , m_main_widget(main_widget)
+{
+    set_frame_thickness(2);
+    set_frame_shadow(FrameShadow::Sunken);
+    set_frame_shape(FrameShape::Container);
+    set_layout(make<GBoxLayout>(Orientation::Vertical));
+    set_fill_with_background_color(true);
+
+    m_labels_container = GWidget::construct(this);
+    m_labels_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
+    m_labels_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
+    m_labels_container->set_preferred_size(0, 20);
+
+    m_octave_label = GLabel::construct("Octave", m_labels_container);
+    m_wave_label = GLabel::construct("Wave", m_labels_container);
+    m_decay_label = GLabel::construct("Decay", m_labels_container);
+    m_delay_label = GLabel::construct("Delay", m_labels_container);
+
+    m_values_container = GWidget::construct(this);
+    m_values_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
+    m_values_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
+    m_values_container->set_preferred_size(0, 10);
+
+    m_octave_value = GLabel::construct(String::number(m_audio_engine.octave()), m_values_container);
+    m_wave_value = GLabel::construct(wave_strings[m_audio_engine.wave()], m_values_container);
+    m_decay_value = GLabel::construct(String::number(m_audio_engine.decay()), m_values_container);
+    m_delay_value = GLabel::construct(String::number(m_audio_engine.delay() / m_audio_engine.tick()), m_values_container);
+
+    m_knobs_container = GWidget::construct(this);
+    m_knobs_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
+
+    // FIXME: Implement vertical flipping in GSlider, not here.
+
+    m_octave_knob = GSlider::construct(Orientation::Vertical, m_knobs_container);
+    m_octave_knob->set_tooltip("Z: octave down, X: octave up");
+    m_octave_knob->set_range(octave_min - 1, octave_max - 1);
+    m_octave_knob->set_value(m_audio_engine.octave() - 1);
+    m_octave_knob->on_value_changed = [this](int value) {
+        int new_octave = octave_max - value;
+        if (m_change_octave)
+            m_main_widget.set_octave_and_ensure_note_change(new_octave == m_audio_engine.octave() + 1 ? Up : Down);
+        ASSERT(new_octave == m_audio_engine.octave());
+        m_octave_value->set_text(String::number(new_octave));
+    };
+
+    m_wave_knob = GSlider::construct(Orientation::Vertical, m_knobs_container);
+    m_wave_knob->set_tooltip("C: cycle through waveforms");
+    m_wave_knob->set_range(0, last_wave);
+    m_wave_knob->set_value(last_wave - m_audio_engine.wave());
+    m_wave_knob->on_value_changed = [this](int value) {
+        int new_wave = last_wave - value;
+        m_audio_engine.set_wave(new_wave);
+        ASSERT(new_wave == m_audio_engine.wave());
+        m_wave_value->set_text(wave_strings[new_wave]);
+    };
+
+    constexpr int max_decay = 20;
+    m_decay_knob = GSlider::construct(Orientation::Vertical, m_knobs_container);
+    m_decay_knob->set_range(0, max_decay);
+    m_decay_knob->set_value(max_decay);
+    m_decay_knob->on_value_changed = [this](int value) {
+        int new_decay = max_decay - value;
+        m_audio_engine.set_decay(new_decay);
+        ASSERT(new_decay == m_audio_engine.decay());
+        m_decay_value->set_text(String::number(new_decay));
+    };
+
+    constexpr int max_delay = 8;
+    m_delay_knob = GSlider::construct(Orientation::Vertical, m_knobs_container);
+    m_delay_knob->set_range(0, max_delay);
+    m_delay_knob->set_value(max_delay);
+    m_delay_knob->on_value_changed = [this](int value) {
+        int new_delay = m_audio_engine.tick() * (max_delay - value);
+        m_audio_engine.set_delay(new_delay);
+        ASSERT(new_delay == m_audio_engine.delay());
+        m_delay_value->set_text(String::number(new_delay / m_audio_engine.tick()));
+    };
+}
+
+KnobsWidget::~KnobsWidget()
+{
+}
+
+void KnobsWidget::update_knobs()
+{
+    m_wave_knob->set_value(last_wave - m_audio_engine.wave());
+
+    // FIXME: This is needed because when the slider is changed directly, it
+    // needs to change the octave, but if the octave was changed elsewhere, we
+    // need to change the slider without changing the octave.
+    m_change_octave = false;
+    m_octave_knob->set_value(octave_max - m_audio_engine.octave());
+    m_change_octave = true;
+}

+ 69 - 0
Applications/Piano/KnobsWidget.h

@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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/GFrame.h>
+
+class GSlider;
+class GLabel;
+class AudioEngine;
+class MainWidget;
+
+class KnobsWidget final : public GFrame {
+    C_OBJECT(KnobsWidget)
+public:
+    virtual ~KnobsWidget() override;
+
+    void update_knobs();
+
+private:
+    KnobsWidget(GWidget* parent, AudioEngine&, MainWidget&);
+
+    AudioEngine& m_audio_engine;
+    MainWidget& m_main_widget;
+
+    RefPtr<GWidget> m_labels_container;
+    RefPtr<GLabel> m_octave_label;
+    RefPtr<GLabel> m_wave_label;
+    RefPtr<GLabel> m_decay_label;
+    RefPtr<GLabel> m_delay_label;
+
+    RefPtr<GWidget> m_values_container;
+    RefPtr<GLabel> m_octave_value;
+    RefPtr<GLabel> m_wave_value;
+    RefPtr<GLabel> m_decay_value;
+    RefPtr<GLabel> m_delay_value;
+
+    RefPtr<GWidget> m_knobs_container;
+    RefPtr<GSlider> m_octave_knob;
+    RefPtr<GSlider> m_wave_knob;
+    RefPtr<GSlider> m_decay_knob;
+    RefPtr<GSlider> m_delay_knob;
+
+    bool m_change_octave { true };
+};

+ 142 - 0
Applications/Piano/MainWidget.cpp

@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "MainWidget.h"
+#include "AudioEngine.h"
+#include "KeysWidget.h"
+#include "KnobsWidget.h"
+#include "RollWidget.h"
+#include "WaveWidget.h"
+#include <LibGUI/GBoxLayout.h>
+
+MainWidget::MainWidget(AudioEngine& audio_engine)
+    : m_audio_engine(audio_engine)
+{
+    set_layout(make<GBoxLayout>(Orientation::Vertical));
+    layout()->set_spacing(2);
+    layout()->set_margins({ 2, 2, 2, 2 });
+    set_fill_with_background_color(true);
+
+    m_wave_widget = WaveWidget::construct(this, audio_engine);
+    m_wave_widget->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
+    m_wave_widget->set_preferred_size(0, 100);
+
+    m_roll_widget = RollWidget::construct(this, audio_engine);
+    m_roll_widget->set_size_policy(SizePolicy::Fill, SizePolicy::Fill);
+    m_roll_widget->set_preferred_size(0, 300);
+
+    m_keys_and_knobs_container = GWidget::construct(this);
+    m_keys_and_knobs_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
+    m_keys_and_knobs_container->layout()->set_spacing(2);
+    m_keys_and_knobs_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
+    m_keys_and_knobs_container->set_preferred_size(0, 100);
+    m_keys_and_knobs_container->set_fill_with_background_color(true);
+
+    m_keys_widget = KeysWidget::construct(m_keys_and_knobs_container, audio_engine);
+
+    m_knobs_widget = KnobsWidget::construct(m_keys_and_knobs_container, audio_engine, *this);
+    m_knobs_widget->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
+    m_knobs_widget->set_preferred_size(200, 0);
+}
+
+MainWidget::~MainWidget()
+{
+}
+
+// FIXME: There are some unnecessary calls to update() throughout this program,
+// which are an easy target for optimization.
+
+void MainWidget::custom_event(CCustomEvent&)
+{
+    m_wave_widget->update();
+
+    if (m_audio_engine.time() == 0)
+        m_roll_widget->update_roll();
+}
+
+void MainWidget::keydown_event(GKeyEvent& event)
+{
+    // This is to stop held-down keys from creating multiple events.
+    if (m_keys_pressed[event.key()])
+        return;
+    else
+        m_keys_pressed[event.key()] = true;
+
+    note_key_action(event.key(), On);
+    special_key_action(event.key());
+    m_keys_widget->update();
+}
+
+void MainWidget::keyup_event(GKeyEvent& event)
+{
+    m_keys_pressed[event.key()] = false;
+
+    note_key_action(event.key(), Off);
+    m_keys_widget->update();
+}
+
+void MainWidget::note_key_action(int key_code, Switch switch_note)
+{
+    int key = m_keys_widget->key_code_to_key(key_code);
+    m_keys_widget->set_key(key, switch_note);
+}
+
+void MainWidget::special_key_action(int key_code)
+{
+    switch (key_code) {
+    case Key_Z:
+        set_octave_and_ensure_note_change(Down);
+        break;
+    case Key_X:
+        set_octave_and_ensure_note_change(Up);
+        break;
+    case Key_C:
+        m_audio_engine.set_wave(Up);
+        m_knobs_widget->update_knobs();
+        break;
+    }
+}
+
+void MainWidget::set_octave_and_ensure_note_change(Direction direction)
+{
+    m_keys_widget->set_key(m_keys_widget->mouse_note(), Off);
+    for (int i = 0; i < key_code_count; ++i) {
+        if (m_keys_pressed[i])
+            note_key_action(i, Off);
+    }
+
+    m_audio_engine.set_octave(direction);
+
+    m_keys_widget->set_key(m_keys_widget->mouse_note(), On);
+    for (int i = 0; i < key_code_count; ++i) {
+        if (m_keys_pressed[i])
+            note_key_action(i, On);
+    }
+
+    m_knobs_widget->update_knobs();
+    m_keys_widget->update();
+}

+ 65 - 0
Applications/Piano/MainWidget.h

@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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/GWidget.h>
+#include <Music.h>
+
+class AudioEngine;
+class WaveWidget;
+class RollWidget;
+class KeysWidget;
+class KnobsWidget;
+
+class MainWidget final : public GWidget {
+    C_OBJECT(MainWidget)
+public:
+    virtual ~MainWidget() override;
+
+    void set_octave_and_ensure_note_change(Direction);
+
+private:
+    explicit MainWidget(AudioEngine&);
+
+    virtual void keydown_event(GKeyEvent&) override;
+    virtual void keyup_event(GKeyEvent&) override;
+    virtual void custom_event(CCustomEvent&) override;
+
+    void note_key_action(int key_code, Switch);
+    void special_key_action(int key_code);
+
+    AudioEngine& m_audio_engine;
+
+    RefPtr<WaveWidget> m_wave_widget;
+    RefPtr<RollWidget> m_roll_widget;
+    RefPtr<GWidget> m_keys_and_knobs_container;
+    RefPtr<KeysWidget> m_keys_widget;
+    RefPtr<KnobsWidget> m_knobs_widget;
+
+    bool m_keys_pressed[key_code_count] { false };
+};

+ 6 - 1
Applications/Piano/Makefile

@@ -1,5 +1,10 @@
 OBJS = \
-    PianoWidget.o \
+    AudioEngine.o \
+    MainWidget.o \
+    WaveWidget.o \
+    RollWidget.o \
+    KeysWidget.o \
+    KnobsWidget.o \
     main.o
 
 PROGRAM = Piano

+ 165 - 41
Applications/Piano/Music.h

@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -27,62 +28,185 @@
 #pragma once
 
 #include <AK/Types.h>
+#include <LibDraw/Color.h>
 
 namespace Music {
 
+// CD quality
+// - Stereo
+// - 16 bit
+// - 44,100 samples/sec
+// - 1,411.2 kbps
+
 struct Sample {
     i16 left;
     i16 right;
 };
 
-enum WaveType { Sine, Saw, Square, Triangle, Noise, InvalidWave };
+constexpr int sample_count = 1024;
+
+constexpr int buffer_size = sample_count * sizeof(Sample);
+
+constexpr double sample_rate = 44100;
+
+constexpr double volume = 1800;
 
-enum PianoKey {
-    K_None,
-    K_C1, K_Db1, K_D1, K_Eb1, K_E1, K_F1, K_Gb1, K_G1, K_Ab1, K_A1, K_Bb1, K_B1,
-    K_C2, K_Db2, K_D2, K_Eb2, K_E2, K_F2, K_Gb2, K_G2,
+enum Switch {
+    Off,
+    On,
 };
 
-inline bool is_white(PianoKey n)
-{
-    switch (n) {
-    case K_C1:
-    case K_D1:
-    case K_E1:
-    case K_F1:
-    case K_G1:
-    case K_A1:
-    case K_B1:
-    case K_C2:
-    case K_D2:
-    case K_E2:
-    case K_F2:
-    case K_G2:
-        return true;
-    default:
-        return false;
-    }
-}
+enum Direction {
+    Down,
+    Up,
+};
+
+enum Wave {
+    Sine,
+    Triangle,
+    Square,
+    Saw,
+    Noise,
+};
 
-enum Note {
-    C1, Db1, D1, Eb1, E1, F1, Gb1, G1, Ab1, A1, Bb1, B1,
-    C2, Db2, D2, Eb2, E2, F2, Gb2, G2, Ab2, A2, Bb2, B2,
-    C3, Db3, D3, Eb3, E3, F3, Gb3, G3, Ab3, A3, Bb3, B3,
-    C4, Db4, D4, Eb4, E4, F4, Gb4, G4, Ab4, A4, Bb4, B4,
-    C5, Db5, D5, Eb5, E5, F5, Gb5, G5, Ab5, A5, Bb5, B5,
-    C6, Db6, D6, Eb6, E6, F6, Gb6, G6, Ab6, A6, Bb6, B6,
-    C7, Db7, D7, Eb7, E7, F7, Gb7, G7, Ab7, A7, Bb7, B7,
+constexpr const char* wave_strings[] = {
+    "Sine",
+    "Triangle",
+    "Square",
+    "Saw",
+    "Noise",
 };
 
-const double note_frequency[] = {
-    /* Octave 1 */ 32.70, 34.65, 36.71, 38.89, 41.20, 43.65, 46.25, 49.00, 51.91, 55.00, 58.27, 61.74,
-    /* Octave 2 */ 65.41, 69.30, 73.42, 77.78, 82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47,
-    /* Octave 3 */ 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185.00, 196.00, 207.65, 220.00, 233.08, 246.94,
-    /* Octave 4 */ 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392.00, 415.30, 440.00, 466.16, 493.88,
-    /* Octave 5 */ 523.25, 554.37, 587.33, 622.25, 659.25, 698.46, 739.99, 783.99, 830.61, 880.00, 932.33, 987.77,
-    /* Octave 6 */ 1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53,
-    /* Octave 7 */ 2093.00, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520.00, 3729.31, 3951.07,
+constexpr int first_wave = Sine;
+constexpr int last_wave = Noise;
+
+enum KeyColor {
+    White,
+    Black,
+};
+
+constexpr KeyColor key_pattern[] = {
+    White,
+    Black,
+    White,
+    Black,
+    White,
+    White,
+    Black,
+    White,
+    Black,
+    White,
+    Black,
+    White,
+};
+
+const Color note_pressed_color(64, 64, 255);
+const Color column_playing_color(128, 128, 255);
+
+constexpr int notes_per_octave = 12;
+constexpr int white_keys_per_octave = 7;
+constexpr int black_keys_per_octave = 5;
+constexpr int octave_min = 1;
+constexpr int octave_max = 7;
+
+// Equal temperament, A = 440Hz
+// We calculate note frequencies relative to A4:
+// 440.0 * pow(pow(2.0, 1.0 / 12.0), N)
+// Where N is the note distance from A.
+constexpr double note_frequencies[] = {
+    // Octave 1
+    32.703195662574764,
+    34.647828872108946,
+    36.708095989675876,
+    38.890872965260044,
+    41.203444614108669,
+    43.653528929125407,
+    46.249302838954222,
+    48.99942949771858,
+    51.913087197493056,
+    54.999999999999915,
+    58.270470189761156,
+    61.735412657015416,
+    // Octave 2
+    65.406391325149571,
+    69.295657744217934,
+    73.416191979351794,
+    77.781745930520117,
+    82.406889228217381,
+    87.307057858250872,
+    92.4986056779085,
+    97.998858995437217,
+    103.82617439498618,
+    109.99999999999989,
+    116.54094037952237,
+    123.4708253140309,
+    // Octave 3
+    130.8127826502992,
+    138.59131548843592,
+    146.83238395870364,
+    155.56349186104035,
+    164.81377845643485,
+    174.61411571650183,
+    184.99721135581709,
+    195.99771799087452,
+    207.65234878997245,
+    219.99999999999989,
+    233.08188075904488,
+    246.94165062806198,
+    // Octave 4
+    261.62556530059851,
+    277.18263097687202,
+    293.66476791740746,
+    311.12698372208081,
+    329.62755691286986,
+    349.22823143300383,
+    369.99442271163434,
+    391.99543598174927,
+    415.30469757994513,
+    440,
+    466.16376151808993,
+    493.88330125612413,
+    // Octave 5
+    523.25113060119736,
+    554.36526195374427,
+    587.32953583481526,
+    622.25396744416196,
+    659.25511382574007,
+    698.456462866008,
+    739.98884542326903,
+    783.99087196349899,
+    830.60939515989071,
+    880.00000000000034,
+    932.32752303618031,
+    987.76660251224882,
+    // Octave 6
+    1046.5022612023952,
+    1108.7305239074892,
+    1174.659071669631,
+    1244.5079348883246,
+    1318.5102276514808,
+    1396.9129257320169,
+    1479.977690846539,
+    1567.9817439269987,
+    1661.2187903197821,
+    1760.000000000002,
+    1864.6550460723618,
+    1975.5332050244986,
+    // Octave 7
+    2093.0045224047913,
+    2217.4610478149793,
+    2349.3181433392633,
+    2489.0158697766506,
+    2637.020455302963,
+    2793.8258514640347,
+    2959.9553816930793,
+    3135.9634878539991,
+    3322.437580639566,
+    3520.0000000000055,
+    3729.3100921447249,
+    3951.0664100489994,
 };
+constexpr int note_count = sizeof(note_frequencies) / sizeof(double);
 
 }
 

+ 0 - 585
Applications/Piano/PianoWidget.cpp

@@ -1,585 +0,0 @@
-/*
- * 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 "PianoWidget.h"
-#include <AK/Queue.h>
-#include <LibDraw/GraphicsBitmap.h>
-#include <LibGUI/GPainter.h>
-#include <math.h>
-
-PianoWidget::PianoWidget()
-{
-    set_font(Font::default_fixed_width_font());
-}
-
-PianoWidget::~PianoWidget()
-{
-}
-
-void PianoWidget::paint_event(GPaintEvent& event)
-{
-    GPainter painter(*this);
-    painter.add_clip_rect(event.rect());
-
-    painter.fill_rect(event.rect(), Color::Black);
-
-    render_wave(painter);
-    render_piano(painter);
-    render_knobs(painter);
-    render_roll(painter);
-}
-
-void PianoWidget::fill_audio_buffer(uint8_t* stream, int len)
-{
-    if (++m_time == m_tick) {
-        m_time = 0;
-        change_roll_column();
-    }
-
-    m_sample_count = len / sizeof(Sample);
-    memset(stream, 0, len);
-
-    auto* sst = (Sample*)stream;
-    for (int i = 0; i < m_sample_count; ++i) {
-        static const double volume = 1800;
-        for (size_t n = 0; n < (sizeof(m_note_on) / sizeof(u8)); ++n) {
-            if (!m_note_on[n])
-                continue;
-            double val = 0;
-            switch (m_wave_type) {
-            case WaveType::Sine:
-                val = ((volume * m_power[n]) * w_sine(n));
-                break;
-            case WaveType::Saw:
-                val = ((volume * m_power[n]) * w_saw(n));
-                break;
-            case WaveType::Square:
-                val = ((volume * m_power[n]) * w_square(n));
-                break;
-            case WaveType::Triangle:
-                val = ((volume * m_power[n]) * w_triangle(n));
-                break;
-            case WaveType::Noise:
-                val = ((volume * m_power[n]) * w_noise());
-                break;
-            }
-            sst[i].left += val;
-        }
-        sst[i].right = sst[i].left;
-    }
-
-    // Decay pressed notes.
-    if (m_decay_enabled) {
-        for (size_t n = 0; n < (sizeof(m_note_on) / sizeof(u8)); ++n) {
-            if (m_note_on[n])
-                m_power[n] *= 0.965;
-        }
-    }
-
-    static Queue<Sample*> delay_frames;
-    static const int delay_length_in_frames = m_tick * 4;
-
-    if (m_delay_enabled) {
-        if (delay_frames.size() >= delay_length_in_frames) {
-            auto* to_blend = delay_frames.dequeue();
-            for (int i = 0; i < m_sample_count; ++i) {
-                sst[i].left += to_blend[i].left * 0.333333;
-                sst[i].right += to_blend[i].right * 0.333333;
-            }
-            delete[] to_blend;
-        }
-        Sample* frame = new Sample[m_sample_count];
-        memcpy(frame, sst, m_sample_count * sizeof(Sample));
-
-        delay_frames.enqueue(frame);
-    }
-
-    ASSERT(len <= 2048 * (int)sizeof(Sample));
-    memcpy(m_back_buffer, (Sample*)stream, len);
-    swap(m_front_buffer, m_back_buffer);
-}
-
-double PianoWidget::w_sine(size_t n)
-{
-    double pos = note_frequency[n] / 44100.0;
-    double sin_step = pos * 2 * M_PI;
-    double w = sin(m_sin_pos[n]);
-    m_sin_pos[n] += sin_step;
-    return w;
-}
-
-static inline double hax_floor(double t)
-{
-    return (int)t;
-}
-
-double PianoWidget::w_saw(size_t n)
-{
-    double saw_step = note_frequency[n] / 44100.0;
-    double t = m_saw_pos[n];
-    double w = (0.5 - (t - hax_floor(t))) * 2;
-    //printf("w: %g, step: %g\n", w, saw_step);
-    m_saw_pos[n] += saw_step;
-    return w;
-}
-
-double PianoWidget::w_square(size_t n)
-{
-    double pos = note_frequency[n] / 44100.0;
-    double square_step = pos * 2 * M_PI;
-    double w = sin(m_square_pos[n]);
-    if (w > 0)
-        w = 1;
-    else
-        w = -1;
-    //printf("w: %g, step: %g\n", w, square_step);
-    m_square_pos[n] += square_step;
-    return w;
-}
-
-double PianoWidget::w_triangle(size_t n)
-{
-    double triangle_step = note_frequency[n] / 44100.0;
-    double t = m_triangle_pos[n];
-    double w = fabs(fmod((4 * t) + 1, 4) - 2) - 1;
-    m_triangle_pos[n] += triangle_step;
-    return w;
-}
-
-double PianoWidget::w_noise()
-{
-    return (((double)rand() / RAND_MAX) * 2.0) - 1.0;
-}
-
-int PianoWidget::octave_base() const
-{
-    return (m_octave - m_octave_min) * 12;
-}
-
-struct KeyDefinition {
-    int index;
-    PianoKey piano_key;
-    String label;
-    KeyCode key_code;
-};
-
-const KeyDefinition key_definitions[] = {
-    { 0, K_C1, "A", KeyCode::Key_A },
-    { 1, K_D1, "S", KeyCode::Key_S },
-    { 2, K_E1, "D", KeyCode::Key_D },
-    { 3, K_F1, "F", KeyCode::Key_F },
-    { 4, K_G1, "G", KeyCode::Key_G },
-    { 5, K_A1, "H", KeyCode::Key_H },
-    { 6, K_B1, "J", KeyCode::Key_J },
-    { 7, K_C2, "K", KeyCode::Key_K },
-    { 8, K_D2, "L", KeyCode::Key_L },
-    { 9, K_E2, ";", KeyCode::Key_Semicolon },
-    { 10, K_F2, "'", KeyCode::Key_Apostrophe },
-    { 11, K_G2, "r", KeyCode::Key_Return },
-    { 0, K_Db1, "W", KeyCode::Key_W },
-    { 1, K_Eb1, "E", KeyCode::Key_E },
-    { 3, K_Gb1, "T", KeyCode::Key_T },
-    { 4, K_Ab1, "Y", KeyCode::Key_Y },
-    { 5, K_Bb1, "U", KeyCode::Key_U },
-    { 7, K_Db2, "O", KeyCode::Key_O },
-    { 8, K_Eb2, "P", KeyCode::Key_P },
-    { 10, K_Gb2, "]", KeyCode::Key_RightBracket },
-};
-
-void PianoWidget::note(KeyCode key_code, SwitchNote switch_note)
-{
-    for (auto& kd : key_definitions) {
-        if (kd.key_code == key_code) {
-            note(kd.piano_key, switch_note);
-            return;
-        }
-    }
-}
-
-void PianoWidget::note(PianoKey piano_key, SwitchNote switch_note)
-{
-    int n = octave_base() + piano_key;
-
-    if (switch_note == On) {
-        if (m_note_on[n] == 0) {
-            m_sin_pos[n] = 0;
-            m_square_pos[n] = 0;
-            m_saw_pos[n] = 0;
-            m_triangle_pos[n] = 0;
-        }
-        ++m_note_on[n];
-        m_power[n] = 1;
-    } else {
-        if (m_note_on[n] > 1) {
-            --m_note_on[n];
-        } else if (m_note_on[n] == 1) {
-            --m_note_on[n];
-            m_power[n] = 0;
-        }
-    }
-}
-
-void PianoWidget::keydown_event(GKeyEvent& event)
-{
-    if (keys[event.key()])
-        return;
-    keys[event.key()] = true;
-
-    switch (event.key()) {
-    case KeyCode::Key_C:
-
-        if (++m_wave_type == InvalidWave)
-            m_wave_type = 0;
-        break;
-    case KeyCode::Key_V:
-        m_delay_enabled = !m_delay_enabled;
-        break;
-    case KeyCode::Key_B:
-        m_decay_enabled = !m_decay_enabled;
-        break;
-    case KeyCode::Key_Z:
-        if (m_octave > m_octave_min)
-            --m_octave;
-        memset(m_note_on, 0, sizeof(m_note_on));
-        break;
-    case KeyCode::Key_X:
-        if (m_octave < m_octave_max)
-            ++m_octave;
-        memset(m_note_on, 0, sizeof(m_note_on));
-        break;
-    default:
-        note((KeyCode)event.key(), On);
-    }
-
-    update();
-}
-
-void PianoWidget::keyup_event(GKeyEvent& event)
-{
-    keys[event.key()] = false;
-    note((KeyCode)event.key(), Off);
-    update();
-}
-
-void PianoWidget::mousedown_event(GMouseEvent& event)
-{
-    m_mouse_pressed = true;
-
-    m_piano_key_under_mouse = find_key_for_relative_position(event.x() - x(), event.y() - y());
-    if (m_piano_key_under_mouse) {
-        note(m_piano_key_under_mouse, On);
-        update();
-        return;
-    }
-
-    RollNote* roll_note_under_mouse = find_roll_note_for_relative_position(event.x() - x(), event.y() - y());
-    if (roll_note_under_mouse)
-        roll_note_under_mouse->pressed = !roll_note_under_mouse->pressed;
-    update();
-}
-
-void PianoWidget::mouseup_event(GMouseEvent&)
-{
-    m_mouse_pressed = false;
-
-    note(m_piano_key_under_mouse, Off);
-    update();
-}
-
-void PianoWidget::mousemove_event(GMouseEvent& event)
-{
-    if (!m_mouse_pressed)
-        return;
-
-    PianoKey mouse_was_over = m_piano_key_under_mouse;
-
-    m_piano_key_under_mouse = find_key_for_relative_position(event.x() - x(), event.y() - y());
-
-    if (m_piano_key_under_mouse == mouse_was_over)
-        return;
-
-    if (mouse_was_over)
-        note(mouse_was_over, Off);
-    if (m_piano_key_under_mouse)
-        note(m_piano_key_under_mouse, On);
-    update();
-}
-
-void PianoWidget::render_wave(GPainter& painter)
-{
-    Color wave_color;
-    switch (m_wave_type) {
-    case WaveType::Sine:
-        wave_color = Color(255, 192, 0);
-        break;
-    case WaveType::Saw:
-        wave_color = Color(240, 100, 128);
-        break;
-    case WaveType::Square:
-        wave_color = Color(128, 160, 255);
-        break;
-    case WaveType::Triangle:
-        wave_color = Color(35, 171, 35);
-        break;
-    case WaveType::Noise:
-        wave_color = Color(197, 214, 225);
-        break;
-    }
-
-    int prev_x = 0;
-    int prev_y = m_height / 2;
-    for (int x = 0; x < m_sample_count; ++x) {
-        double val = m_front_buffer[x].left;
-        val /= 32768;
-        val *= m_height;
-        int y = ((m_height / 8) - 8) + val;
-        if (x == 0)
-            painter.set_pixel({ x, y }, wave_color);
-        else
-            painter.draw_line({ prev_x, prev_y }, { x, y }, wave_color);
-        prev_x = x;
-        prev_y = y;
-    }
-}
-
-static int white_key_width = 22;
-static int white_key_height = 60;
-static int black_key_width = 16;
-static int black_key_height = 35;
-static int black_key_stride = white_key_width - black_key_width;
-static int black_key_offset = white_key_width - black_key_width / 2;
-
-Rect PianoWidget::define_piano_key_rect(int index, PianoKey n) const
-{
-    Rect rect;
-    int stride = 0;
-    int offset = 0;
-    if (is_white(n)) {
-        rect.set_width(white_key_width);
-        rect.set_height(white_key_height);
-    } else {
-        rect.set_width(black_key_width);
-        rect.set_height(black_key_height);
-        stride = black_key_stride;
-        offset = black_key_offset;
-    }
-    rect.set_x(offset + index * rect.width() + (index * stride));
-    rect.set_y(m_height - white_key_height);
-    return rect;
-}
-
-PianoKey PianoWidget::find_key_for_relative_position(int x, int y) const
-{
-    // here we iterate backwards because we want to try to match the black
-    // keys first, which are defined last
-    for (int i = (sizeof(key_definitions) / sizeof(KeyDefinition)) - 1; i >= 0; i--) {
-        auto& kd = key_definitions[i];
-
-        auto rect = define_piano_key_rect(kd.index, kd.piano_key);
-
-        if (rect.contains(x, y))
-            return kd.piano_key;
-    }
-
-    return K_None;
-}
-
-void PianoWidget::render_piano_key(GPainter& painter, int index, PianoKey n, const StringView& text)
-{
-    Color color;
-    if (m_note_on[octave_base() + n]) {
-        color = Color(64, 64, 255);
-    } else {
-        if (is_white(n))
-            color = Color::White;
-        else
-            color = Color::Black;
-    }
-
-    auto rect = define_piano_key_rect(index, n);
-
-    painter.fill_rect(rect, color);
-    painter.draw_rect(rect, Color::Black);
-
-    Color text_color;
-    if (is_white(n)) {
-        text_color = Color::Black;
-    } else {
-        text_color = Color::White;
-    }
-    Rect r(rect.x(), rect.y() + rect.height() / 2, rect.width(), rect.height() / 2);
-    painter.draw_text(r, text, TextAlignment::Center, text_color);
-}
-
-void PianoWidget::render_piano(GPainter& painter)
-{
-    for (auto& kd : key_definitions)
-        render_piano_key(painter, kd.index, kd.piano_key, kd.label);
-}
-
-static int knob_width = 100;
-
-void PianoWidget::render_knob(GPainter& painter, const Rect& rect, bool state, const StringView& text)
-{
-    Color text_color;
-    if (state) {
-        painter.fill_rect(rect, Color(0, 200, 0));
-        text_color = Color::Black;
-    } else {
-        painter.draw_rect(rect, Color(180, 0, 0));
-        text_color = Color(180, 0, 0);
-    }
-    painter.draw_text(rect, text, TextAlignment::Center, text_color);
-}
-
-void PianoWidget::render_knobs(GPainter& painter)
-{
-    Rect delay_knob_rect(m_width - knob_width - 16, m_height - 50, knob_width, 16);
-    render_knob(painter, delay_knob_rect, m_delay_enabled, "V: Delay   ");
-
-    Rect decay_knob_rect(m_width - knob_width - 16, m_height - 30, knob_width, 16);
-    render_knob(painter, decay_knob_rect, m_decay_enabled, "B: Decay   ");
-
-    Rect octave_knob_rect(m_width - knob_width - 16 - knob_width - 16, m_height - 50, knob_width, 16);
-    auto text = String::format("Z/X: Oct %d ", m_octave);
-    int oct_rgb_step = 255 / (m_octave_max + 4);
-    int oshade = (m_octave + 4) * oct_rgb_step;
-    painter.draw_rect(octave_knob_rect, Color(oshade, oshade, oshade));
-    painter.draw_text(octave_knob_rect, text, TextAlignment::Center, Color(oshade, oshade, oshade));
-
-    Rect wave_knob_rect(m_width - knob_width - 16 - knob_width - 16, m_height - 30, knob_width, 16);
-    switch (m_wave_type) {
-    case WaveType::Sine:
-        painter.draw_rect(wave_knob_rect, Color(255, 192, 0));
-        painter.draw_text(wave_knob_rect, "C: Sine    ", TextAlignment::Center, Color(255, 192, 0));
-        break;
-    case WaveType::Saw:
-        painter.draw_rect(wave_knob_rect, Color(240, 100, 128));
-        painter.draw_text(wave_knob_rect, "C: Sawtooth", TextAlignment::Center, Color(240, 100, 128));
-        break;
-    case WaveType::Square:
-        painter.draw_rect(wave_knob_rect, Color(128, 160, 255));
-        painter.draw_text(wave_knob_rect, "C: Square  ", TextAlignment::Center, Color(128, 160, 255));
-        break;
-    case WaveType::Triangle:
-        painter.draw_rect(wave_knob_rect, Color(35, 171, 35));
-        painter.draw_text(wave_knob_rect, "C: Triangle", TextAlignment::Center, Color(35, 171, 35));
-        break;
-    case WaveType::Noise:
-        painter.draw_rect(wave_knob_rect, Color(197, 214, 225));
-        painter.draw_text(wave_knob_rect, "C: Noise   ", TextAlignment::Center, Color(197, 214, 225));
-        break;
-    }
-}
-
-static int roll_columns = 32;
-static int roll_rows = 20;
-static int roll_note_size = 512 / roll_columns;
-static int roll_height = roll_note_size * roll_rows;
-static int roll_y = 512 - white_key_height - roll_height - 16;
-
-Rect PianoWidget::define_roll_note_rect(int column, int row) const
-{
-    Rect rect;
-    rect.set_width(roll_note_size);
-    rect.set_height(roll_note_size);
-    rect.set_x(column * roll_note_size);
-    rect.set_y(roll_y + (row * roll_note_size));
-
-    return rect;
-}
-
-PianoWidget::RollNote* PianoWidget::find_roll_note_for_relative_position(int x, int y)
-{
-    for (int row = 0; row < roll_rows; ++row) {
-        for (int column = 0; column < roll_columns; ++column) {
-            auto rect = define_roll_note_rect(column, row);
-            if (rect.contains(x, y))
-                return &m_roll_notes[row][column];
-        }
-    }
-
-    return nullptr;
-}
-
-void PianoWidget::render_roll_note(GPainter& painter, int column, int row, PianoKey key)
-{
-    Color color;
-    auto roll_note = m_roll_notes[row][column];
-    if (roll_note.pressed) {
-        if (roll_note.playing)
-            color = Color(24, 24, 255);
-        else
-            color = Color(64, 64, 255);
-    } else {
-        if (roll_note.playing)
-            color = Color(104, 104, 255);
-        else
-            color = is_white(key) ? Color::White : Color::MidGray;
-    }
-
-    auto rect = define_roll_note_rect(column, row);
-
-    painter.fill_rect(rect, color);
-    painter.draw_rect(rect, Color::Black);
-}
-
-void PianoWidget::render_roll(GPainter& painter)
-{
-    for (int row = 0; row < roll_rows; ++row) {
-        PianoKey key = (PianoKey)(roll_rows - row);
-        for (int column = 0; column < roll_columns; ++column)
-            render_roll_note(painter, column, row, key);
-    }
-}
-
-void PianoWidget::change_roll_column()
-{
-    static int current_column = 0;
-    static int previous_column = roll_columns - 1;
-
-    for (int row = 0; row < roll_rows; ++row) {
-        m_roll_notes[row][previous_column].playing = false;
-        if (m_roll_notes[row][previous_column].pressed)
-            note((PianoKey)(roll_rows - row), Off);
-
-        m_roll_notes[row][current_column].playing = true;
-        if (m_roll_notes[row][current_column].pressed)
-            note((PianoKey)(roll_rows - row), On);
-    }
-
-    if (++current_column == roll_columns)
-        current_column = 0;
-    if (++previous_column == roll_columns)
-        previous_column = 0;
-
-    update();
-}
-
-void PianoWidget::custom_event(CCustomEvent&)
-{
-    update();
-}

+ 0 - 121
Applications/Piano/PianoWidget.h

@@ -1,121 +0,0 @@
-/*
- * 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 "Music.h"
-#include <LibGUI/GWidget.h>
-
-class GPainter;
-
-class PianoWidget final : public GWidget {
-    C_OBJECT(PianoWidget)
-public:
-    virtual ~PianoWidget() override;
-
-    void fill_audio_buffer(uint8_t* stream, int len);
-
-private:
-    PianoWidget();
-    virtual void paint_event(GPaintEvent&) override;
-    virtual void keydown_event(GKeyEvent&) override;
-    virtual void keyup_event(GKeyEvent&) override;
-    virtual void custom_event(CCustomEvent&) override;
-    virtual void mousedown_event(GMouseEvent&) override;
-    virtual void mouseup_event(GMouseEvent&) override;
-    virtual void mousemove_event(GMouseEvent&) override;
-
-    double w_sine(size_t);
-    double w_saw(size_t);
-    double w_square(size_t);
-    double w_triangle(size_t);
-    double w_noise();
-
-    struct RollNote {
-        bool pressed;
-        bool playing;
-    };
-
-    Rect define_piano_key_rect(int index, PianoKey) const;
-    PianoKey find_key_for_relative_position(int x, int y) const;
-    Rect define_roll_note_rect(int column, int row) const;
-    RollNote* find_roll_note_for_relative_position(int x, int y);
-
-    void render_wave(GPainter&);
-    void render_piano_key(GPainter&, int index, PianoKey, const StringView&);
-    void render_piano(GPainter&);
-    void render_knobs(GPainter&);
-    void render_knob(GPainter&, const Rect&, bool state, const StringView&);
-    void render_roll_note(GPainter&, int column, int row, PianoKey);
-    void render_roll(GPainter&);
-
-    void change_roll_column();
-
-    enum SwitchNote {
-        Off,
-        On
-    };
-    void note(KeyCode, SwitchNote);
-    void note(PianoKey, SwitchNote);
-
-    int octave_base() const;
-
-    int m_sample_count { 0 };
-    Sample m_front[2048] { 0, 0 };
-    Sample m_back[2048] { 0, 0 };
-    Sample* m_front_buffer { m_front };
-    Sample* m_back_buffer { m_back };
-
-#define note_count sizeof(note_frequency) / sizeof(double)
-
-    u8 m_note_on[note_count] { 0 };
-    double m_power[note_count];
-    double m_sin_pos[note_count];
-    double m_square_pos[note_count];
-    double m_saw_pos[note_count];
-    double m_triangle_pos[note_count];
-
-    int m_octave_min { 1 };
-    int m_octave_max { 6 };
-    int m_octave { 4 };
-
-    int m_width { 512 };
-    int m_height { 512 };
-
-    int m_wave_type { 0 };
-    bool m_delay_enabled { false };
-    bool m_decay_enabled { false };
-
-    bool keys[256] { false };
-
-    PianoKey m_piano_key_under_mouse { K_None };
-    bool m_mouse_pressed { false };
-
-    RollNote m_roll_notes[20][32] { { false, false } };
-
-    int m_time { 0 };
-    int m_tick { 10 };
-};

+ 154 - 0
Applications/Piano/RollWidget.cpp

@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "RollWidget.h"
+#include "AudioEngine.h"
+#include <LibGUI/GPainter.h>
+#include <LibGUI/GScrollBar.h>
+
+constexpr int note_height = 20;
+constexpr int roll_height = note_count * note_height;
+
+RollWidget::RollWidget(GWidget* parent, AudioEngine& audio_engine)
+    : GScrollableWidget(parent)
+    , m_audio_engine(audio_engine)
+{
+    set_frame_thickness(2);
+    set_frame_shadow(FrameShadow::Sunken);
+    set_frame_shape(FrameShape::Container);
+
+    set_should_hide_unnecessary_scrollbars(true);
+    set_content_size({ 0, roll_height });
+    vertical_scrollbar().set_value(roll_height / 2);
+}
+
+RollWidget::~RollWidget()
+{
+}
+
+void RollWidget::paint_event(GPaintEvent& event)
+{
+    int roll_width = widget_inner_rect().width();
+    double note_width = static_cast<double>(roll_width) / m_horizontal_notes;
+
+    set_content_size({ roll_width, roll_height });
+
+    // This calculates the minimum number of rows needed. We account for a
+    // partial row at the top and/or bottom.
+    int y_offset = vertical_scrollbar().value();
+    int note_offset = y_offset / note_height;
+    int note_offset_remainder = y_offset % note_height;
+    int paint_area = widget_inner_rect().height() + note_offset_remainder;
+    if (paint_area % note_height != 0)
+        paint_area += note_height;
+    int notes_to_paint = paint_area / note_height;
+    int key_pattern_index = (notes_per_octave - 1) - (note_offset % notes_per_octave);
+
+    GPainter painter(*this);
+    painter.translate(frame_thickness(), frame_thickness());
+    painter.translate(0, -note_offset_remainder);
+
+    for (int y = 0; y < notes_to_paint; ++y) {
+        int y_pos = y * note_height;
+        for (int x = 0; x < m_horizontal_notes; ++x) {
+            // This is needed to avoid rounding errors. You can't just use
+            // note_width as the width.
+            int x_pos = x * note_width;
+            int next_x_pos = (x + 1) * note_width;
+            int distance_to_next_x = next_x_pos - x_pos;
+            Rect rect(x_pos, y_pos, distance_to_next_x, note_height);
+
+            if (m_roll_notes[y + note_offset][x] == On)
+                painter.fill_rect(rect, note_pressed_color);
+            else if (x == m_current_column)
+                painter.fill_rect(rect, column_playing_color);
+            else if (key_pattern[key_pattern_index] == Black)
+                painter.fill_rect(rect, Color::LightGray);
+            else
+                painter.fill_rect(rect, Color::White);
+
+            painter.draw_line(rect.top_right(), rect.bottom_right(), Color::Black);
+            painter.draw_line(rect.bottom_left(), rect.bottom_right(), Color::Black);
+        }
+
+        if (--key_pattern_index == -1)
+            key_pattern_index = notes_per_octave - 1;
+    }
+
+    GFrame::paint_event(event);
+}
+
+void RollWidget::mousedown_event(GMouseEvent& event)
+{
+    if (!widget_inner_rect().contains(event.x(), event.y()))
+        return;
+
+    int roll_width = widget_inner_rect().width();
+    double note_width = static_cast<double>(roll_width) / m_horizontal_notes;
+
+    int y = (event.y() + vertical_scrollbar().value()) - frame_thickness();
+    y /= note_height;
+
+    // There's a case where we can't just use x / note_width. For example, if
+    // your note_width is 3.1 you will have a rect starting at 3. When that
+    // leftmost pixel of the rect is clicked you will do 3 / 3.1 which is 0
+    // and not 1. We can avoid that case by shifting x by 1 if note_width is
+    // fractional, being careful not to shift out of bounds.
+    int x = event.x() - frame_thickness();
+    bool note_width_is_fractional = note_width - static_cast<int>(note_width) != 0;
+    bool x_is_not_last = x != widget_inner_rect().width() - 1;
+    if (note_width_is_fractional && x_is_not_last)
+        ++x;
+    x /= note_width;
+
+    if (m_roll_notes[y][x] == On) {
+        if (x == m_current_column) // If you turn off a note that is playing.
+            m_audio_engine.set_note((note_count - 1) - y, Off);
+        m_roll_notes[y][x] = Off;
+    } else {
+        m_roll_notes[y][x] = On;
+    }
+
+    update();
+}
+
+void RollWidget::update_roll()
+{
+    if (++m_current_column == m_horizontal_notes)
+        m_current_column = 0;
+    if (++m_previous_column == m_horizontal_notes)
+        m_previous_column = 0;
+
+    for (int note = 0; note < note_count; ++note) {
+        if (m_roll_notes[note][m_previous_column] == On)
+            m_audio_engine.set_note((note_count - 1) - note, Off);
+        if (m_roll_notes[note][m_current_column] == On)
+            m_audio_engine.set_note((note_count - 1) - note, On);
+    }
+
+    update();
+}

+ 54 - 0
Applications/Piano/RollWidget.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "Music.h"
+#include <LibGUI/GScrollableWidget.h>
+
+class AudioEngine;
+
+class RollWidget final : public GScrollableWidget {
+    C_OBJECT(RollWidget)
+public:
+    virtual ~RollWidget() override;
+
+    void update_roll();
+
+private:
+    RollWidget(GWidget* parent, AudioEngine&);
+
+    virtual void paint_event(GPaintEvent&) override;
+    virtual void mousedown_event(GMouseEvent& event) override;
+
+    AudioEngine& m_audio_engine;
+
+    int m_horizontal_notes { 32 };
+    Switch m_roll_notes[note_count][32] { Off };
+    int m_current_column { 0 };
+    int m_previous_column { m_horizontal_notes - 1 };
+};

+ 114 - 0
Applications/Piano/WaveWidget.cpp

@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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 "WaveWidget.h"
+#include "AudioEngine.h"
+#include <LibGUI/GPainter.h>
+#include <limits>
+
+WaveWidget::WaveWidget(GWidget* parent, AudioEngine& audio_engine)
+    : GFrame(parent)
+    , m_audio_engine(audio_engine)
+{
+    set_frame_thickness(2);
+    set_frame_shadow(FrameShadow::Sunken);
+    set_frame_shape(FrameShape::Container);
+}
+
+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();
+    double percentage = sample / sample_max;
+    double portion_of_height = percentage * frame_inner_rect().height();
+    int y = (frame_inner_rect().height() / 2) + portion_of_height;
+    return y;
+}
+
+void WaveWidget::paint_event(GPaintEvent& event)
+{
+    GPainter painter(*this);
+    painter.fill_rect(frame_inner_rect(), Color::Black);
+    painter.translate(frame_thickness(), frame_thickness());
+
+    Color wave_color = wave_colors[m_audio_engine.wave()];
+    auto buffer = m_audio_engine.buffer();
+    double width_scale = static_cast<double>(frame_inner_rect().width()) / buffer.size();
+
+    int prev_x = 0;
+    int prev_y = sample_to_y(buffer[0].left);
+    painter.set_pixel({ prev_x, prev_y }, wave_color);
+
+    for (size_t x = 1; x < buffer.size(); ++x) {
+        int y = sample_to_y(buffer[x].left);
+
+        Point point1(prev_x * width_scale, prev_y);
+        Point point2(x * width_scale, y);
+        painter.draw_line(point1, point2, wave_color);
+
+        prev_x = x;
+        prev_y = y;
+    }
+
+    GFrame::paint_event(event);
+}

+ 48 - 0
Applications/Piano/WaveWidget.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-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/GFrame.h>
+
+class GPainter;
+class AudioEngine;
+
+class WaveWidget final : public GFrame {
+    C_OBJECT(WaveWidget)
+public:
+    virtual ~WaveWidget() override;
+
+private:
+    WaveWidget(GWidget* parent, AudioEngine&);
+
+    virtual void paint_event(GPaintEvent&) override;
+
+    int sample_to_y(int sample) const;
+
+    AudioEngine& m_audio_engine;
+};

+ 16 - 13
Applications/Piano/main.cpp

@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -24,8 +25,8 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#include "Music.h"
-#include "PianoWidget.h"
+#include "AudioEngine.h"
+#include "MainWidget.h"
 #include <LibAudio/AClientConnection.h>
 #include <LibCore/CFile.h>
 #include <LibDraw/PNGLoader.h>
@@ -39,34 +40,36 @@
 int main(int argc, char** argv)
 {
     GApplication app(argc, argv);
+
     auto audio_client = AClientConnection::construct();
     audio_client->handshake();
 
+    AudioEngine audio_engine;
+
     auto window = GWindow::construct();
+    auto main_widget = MainWidget::construct(audio_engine);
+    window->set_main_widget(main_widget);
     window->set_title("Piano");
-    window->set_rect(100, 100, 512, 512);
-
-    auto piano_widget = PianoWidget::construct();
-    window->set_main_widget(piano_widget);
-    window->show();
+    window->set_rect(90, 90, 840, 600);
     window->set_icon(load_png("/res/icons/16x16/app-piano.png"));
+    window->show();
 
-    LibThread::Thread sound_thread([piano_widget = piano_widget.ptr()] {
+    LibThread::Thread audio_thread([&] {
         auto audio = CFile::construct("/dev/audio");
         if (!audio->open(CIODevice::WriteOnly)) {
             dbgprintf("Can't open audio device: %s", audio->error_string());
             return 1;
         }
 
+        FixedArray<Sample> buffer(sample_count);
         for (;;) {
-            u8 buffer[4096];
-            piano_widget->fill_audio_buffer(buffer, sizeof(buffer));
-            audio->write(buffer, sizeof(buffer));
-            CEventLoop::current().post_event(*piano_widget, make<CCustomEvent>(0));
+            audio_engine.fill_buffer(buffer);
+            audio->write(reinterpret_cast<u8*>(buffer.data()), buffer_size);
+            CEventLoop::current().post_event(*main_widget, make<CCustomEvent>(0));
             CEventLoop::wake();
         }
     });
-    sound_thread.start();
+    audio_thread.start();
 
     auto menubar = make<GMenuBar>();