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
This commit is contained in:
William McPherson 2020-01-31 03:02:45 +11:00 committed by Andreas Kling
parent ddefb95b21
commit 4a36a51618
Notes: sideshowbarker 2024-07-19 09:44:28 +09:00
17 changed files with 1647 additions and 762 deletions

View file

@ -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;
}

View file

@ -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 };
};

View file

@ -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;
}

View file

@ -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 };
};

View file

@ -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;
}

View file

@ -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 };
};

View file

@ -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();
}

View file

@ -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 };
};

View file

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

View file

@ -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,63 +28,186 @@
#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;
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,
constexpr int buffer_size = sample_count * sizeof(Sample);
constexpr double sample_rate = 44100;
constexpr double volume = 1800;
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 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,
enum Direction {
Down,
Up,
};
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,
enum Wave {
Sine,
Triangle,
Square,
Saw,
Noise,
};
constexpr const char* wave_strings[] = {
"Sine",
"Triangle",
"Square",
"Saw",
"Noise",
};
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);
}
using namespace Music;

View file

@ -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();
}

View file

@ -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 };
};

View file

@ -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();
}

View file

@ -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 };
};

View file

@ -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);
}

View file

@ -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;
};

View file

@ -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>();