mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
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:
parent
ddefb95b21
commit
4a36a51618
Notes:
sideshowbarker
2024-07-19 09:44:28 +09:00
Author: https://github.com/willmcpherson2 Commit: https://github.com/SerenityOS/serenity/commit/4a36a516183 Pull-request: https://github.com/SerenityOS/serenity/pull/1150 Reviewed-by: https://github.com/awesomekling Reviewed-by: https://github.com/bugaevc
17 changed files with 1647 additions and 762 deletions
216
Applications/Piano/AudioEngine.cpp
Normal file
216
Applications/Piano/AudioEngine.cpp
Normal 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;
|
||||
}
|
85
Applications/Piano/AudioEngine.h
Normal file
85
Applications/Piano/AudioEngine.h
Normal 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 };
|
||||
};
|
320
Applications/Piano/KeysWidget.cpp
Normal file
320
Applications/Piano/KeysWidget.cpp
Normal 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;
|
||||
}
|
61
Applications/Piano/KeysWidget.h
Normal file
61
Applications/Piano/KeysWidget.h
Normal 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 };
|
||||
};
|
131
Applications/Piano/KnobsWidget.cpp
Normal file
131
Applications/Piano/KnobsWidget.cpp
Normal 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;
|
||||
}
|
69
Applications/Piano/KnobsWidget.h
Normal file
69
Applications/Piano/KnobsWidget.h
Normal 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 };
|
||||
};
|
142
Applications/Piano/MainWidget.cpp
Normal file
142
Applications/Piano/MainWidget.cpp
Normal 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();
|
||||
}
|
65
Applications/Piano/MainWidget.h
Normal file
65
Applications/Piano/MainWidget.h
Normal 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 };
|
||||
};
|
|
@ -1,5 +1,10 @@
|
|||
OBJS = \
|
||||
PianoWidget.o \
|
||||
AudioEngine.o \
|
||||
MainWidget.o \
|
||||
WaveWidget.o \
|
||||
RollWidget.o \
|
||||
KeysWidget.o \
|
||||
KnobsWidget.o \
|
||||
main.o
|
||||
|
||||
PROGRAM = Piano
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Music.h"
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GPainter;
|
||||
|
||||
class PianoWidget final : public GWidget {
|
||||
C_OBJECT(PianoWidget)
|
||||
public:
|
||||
virtual ~PianoWidget() override;
|
||||
|
||||
void fill_audio_buffer(uint8_t* stream, int len);
|
||||
|
||||
private:
|
||||
PianoWidget();
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void keyup_event(GKeyEvent&) override;
|
||||
virtual void custom_event(CCustomEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
|
||||
double w_sine(size_t);
|
||||
double w_saw(size_t);
|
||||
double w_square(size_t);
|
||||
double w_triangle(size_t);
|
||||
double w_noise();
|
||||
|
||||
struct RollNote {
|
||||
bool pressed;
|
||||
bool playing;
|
||||
};
|
||||
|
||||
Rect define_piano_key_rect(int index, PianoKey) const;
|
||||
PianoKey find_key_for_relative_position(int x, int y) const;
|
||||
Rect define_roll_note_rect(int column, int row) const;
|
||||
RollNote* find_roll_note_for_relative_position(int x, int y);
|
||||
|
||||
void render_wave(GPainter&);
|
||||
void render_piano_key(GPainter&, int index, PianoKey, const StringView&);
|
||||
void render_piano(GPainter&);
|
||||
void render_knobs(GPainter&);
|
||||
void render_knob(GPainter&, const Rect&, bool state, const StringView&);
|
||||
void render_roll_note(GPainter&, int column, int row, PianoKey);
|
||||
void render_roll(GPainter&);
|
||||
|
||||
void change_roll_column();
|
||||
|
||||
enum SwitchNote {
|
||||
Off,
|
||||
On
|
||||
};
|
||||
void note(KeyCode, SwitchNote);
|
||||
void note(PianoKey, SwitchNote);
|
||||
|
||||
int octave_base() const;
|
||||
|
||||
int m_sample_count { 0 };
|
||||
Sample m_front[2048] { 0, 0 };
|
||||
Sample m_back[2048] { 0, 0 };
|
||||
Sample* m_front_buffer { m_front };
|
||||
Sample* m_back_buffer { m_back };
|
||||
|
||||
#define note_count sizeof(note_frequency) / sizeof(double)
|
||||
|
||||
u8 m_note_on[note_count] { 0 };
|
||||
double m_power[note_count];
|
||||
double m_sin_pos[note_count];
|
||||
double m_square_pos[note_count];
|
||||
double m_saw_pos[note_count];
|
||||
double m_triangle_pos[note_count];
|
||||
|
||||
int m_octave_min { 1 };
|
||||
int m_octave_max { 6 };
|
||||
int m_octave { 4 };
|
||||
|
||||
int m_width { 512 };
|
||||
int m_height { 512 };
|
||||
|
||||
int m_wave_type { 0 };
|
||||
bool m_delay_enabled { false };
|
||||
bool m_decay_enabled { false };
|
||||
|
||||
bool keys[256] { false };
|
||||
|
||||
PianoKey m_piano_key_under_mouse { K_None };
|
||||
bool m_mouse_pressed { false };
|
||||
|
||||
RollNote m_roll_notes[20][32] { { false, false } };
|
||||
|
||||
int m_time { 0 };
|
||||
int m_tick { 10 };
|
||||
};
|
154
Applications/Piano/RollWidget.cpp
Normal file
154
Applications/Piano/RollWidget.cpp
Normal 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();
|
||||
}
|
54
Applications/Piano/RollWidget.h
Normal file
54
Applications/Piano/RollWidget.h
Normal 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 };
|
||||
};
|
114
Applications/Piano/WaveWidget.cpp
Normal file
114
Applications/Piano/WaveWidget.cpp
Normal 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);
|
||||
}
|
48
Applications/Piano/WaveWidget.h
Normal file
48
Applications/Piano/WaveWidget.h
Normal 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;
|
||||
};
|
|
@ -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>();
|
||||
|
||||
|
|
Loading…
Reference in a new issue