diff options
-rw-r--r-- | Applications/Piano/AudioEngine.cpp | 216 | ||||
-rw-r--r-- | Applications/Piano/AudioEngine.h | 85 | ||||
-rw-r--r-- | Applications/Piano/KeysWidget.cpp | 320 | ||||
-rw-r--r-- | Applications/Piano/KeysWidget.h | 61 | ||||
-rw-r--r-- | Applications/Piano/KnobsWidget.cpp | 131 | ||||
-rw-r--r-- | Applications/Piano/KnobsWidget.h | 69 | ||||
-rw-r--r-- | Applications/Piano/MainWidget.cpp | 142 | ||||
-rw-r--r-- | Applications/Piano/MainWidget.h | 65 | ||||
-rw-r--r-- | Applications/Piano/Makefile | 7 | ||||
-rw-r--r-- | Applications/Piano/Music.h | 206 | ||||
-rw-r--r-- | Applications/Piano/PianoWidget.cpp | 585 | ||||
-rw-r--r-- | Applications/Piano/PianoWidget.h | 121 | ||||
-rw-r--r-- | Applications/Piano/RollWidget.cpp | 154 | ||||
-rw-r--r-- | Applications/Piano/RollWidget.h | 54 | ||||
-rw-r--r-- | Applications/Piano/WaveWidget.cpp | 114 | ||||
-rw-r--r-- | Applications/Piano/WaveWidget.h | 48 | ||||
-rw-r--r-- | Applications/Piano/main.cpp | 29 |
17 files changed, 1646 insertions, 761 deletions
diff --git a/Applications/Piano/AudioEngine.cpp b/Applications/Piano/AudioEngine.cpp new file mode 100644 index 0000000000..e32028d8ac --- /dev/null +++ b/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; +} diff --git a/Applications/Piano/AudioEngine.h b/Applications/Piano/AudioEngine.h new file mode 100644 index 0000000000..14280a716f --- /dev/null +++ b/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 }; +}; diff --git a/Applications/Piano/KeysWidget.cpp b/Applications/Piano/KeysWidget.cpp new file mode 100644 index 0000000000..36d4ce19e6 --- /dev/null +++ b/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; +} diff --git a/Applications/Piano/KeysWidget.h b/Applications/Piano/KeysWidget.h new file mode 100644 index 0000000000..c8cdb315ae --- /dev/null +++ b/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 }; +}; diff --git a/Applications/Piano/KnobsWidget.cpp b/Applications/Piano/KnobsWidget.cpp new file mode 100644 index 0000000000..0a55467b5f --- /dev/null +++ b/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; +} diff --git a/Applications/Piano/KnobsWidget.h b/Applications/Piano/KnobsWidget.h new file mode 100644 index 0000000000..a2f6f2f0e6 --- /dev/null +++ b/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 }; +}; diff --git a/Applications/Piano/MainWidget.cpp b/Applications/Piano/MainWidget.cpp new file mode 100644 index 0000000000..5a8c006ae7 --- /dev/null +++ b/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(); +} diff --git a/Applications/Piano/MainWidget.h b/Applications/Piano/MainWidget.h new file mode 100644 index 0000000000..5f1fce8afd --- /dev/null +++ b/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 }; +}; diff --git a/Applications/Piano/Makefile b/Applications/Piano/Makefile index f39d2ba9f6..718820b211 100644 --- a/Applications/Piano/Makefile +++ b/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 diff --git a/Applications/Piano/Music.h b/Applications/Piano/Music.h index 26910b456d..716a17ddfc 100644 --- a/Applications/Piano/Music.h +++ b/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); } diff --git a/Applications/Piano/PianoWidget.cpp b/Applications/Piano/PianoWidget.cpp deleted file mode 100644 index 0703c402e5..0000000000 --- a/Applications/Piano/PianoWidget.cpp +++ /dev/null @@ -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(); -} diff --git a/Applications/Piano/PianoWidget.h b/Applications/Piano/PianoWidget.h deleted file mode 100644 index bdeff89d9d..0000000000 --- a/Applications/Piano/PianoWidget.h +++ /dev/null @@ -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 }; -}; diff --git a/Applications/Piano/RollWidget.cpp b/Applications/Piano/RollWidget.cpp new file mode 100644 index 0000000000..4f919ef0fc --- /dev/null +++ b/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(); +} diff --git a/Applications/Piano/RollWidget.h b/Applications/Piano/RollWidget.h new file mode 100644 index 0000000000..91c0c8aefd --- /dev/null +++ b/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 }; +}; diff --git a/Applications/Piano/WaveWidget.cpp b/Applications/Piano/WaveWidget.cpp new file mode 100644 index 0000000000..2c0d014d6f --- /dev/null +++ b/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); +} diff --git a/Applications/Piano/WaveWidget.h b/Applications/Piano/WaveWidget.h new file mode 100644 index 0000000000..1a8d9f72cd --- /dev/null +++ b/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; +}; diff --git a/Applications/Piano/main.cpp b/Applications/Piano/main.cpp index 038a22e626..3d8f0a36b4 100644 --- a/Applications/Piano/main.cpp +++ b/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>(); |