mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 10:47:44 +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
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 = \
|
OBJS = \
|
||||||
PianoWidget.o \
|
AudioEngine.o \
|
||||||
|
MainWidget.o \
|
||||||
|
WaveWidget.o \
|
||||||
|
RollWidget.o \
|
||||||
|
KeysWidget.o \
|
||||||
|
KnobsWidget.o \
|
||||||
main.o
|
main.o
|
||||||
|
|
||||||
PROGRAM = Piano
|
PROGRAM = Piano
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||||
|
* Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -27,63 +28,186 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <AK/Types.h>
|
#include <AK/Types.h>
|
||||||
|
#include <LibDraw/Color.h>
|
||||||
|
|
||||||
namespace Music {
|
namespace Music {
|
||||||
|
|
||||||
|
// CD quality
|
||||||
|
// - Stereo
|
||||||
|
// - 16 bit
|
||||||
|
// - 44,100 samples/sec
|
||||||
|
// - 1,411.2 kbps
|
||||||
|
|
||||||
struct Sample {
|
struct Sample {
|
||||||
i16 left;
|
i16 left;
|
||||||
i16 right;
|
i16 right;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum WaveType { Sine, Saw, Square, Triangle, Noise, InvalidWave };
|
constexpr int sample_count = 1024;
|
||||||
|
|
||||||
enum PianoKey {
|
constexpr int buffer_size = sample_count * sizeof(Sample);
|
||||||
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,
|
constexpr double sample_rate = 44100;
|
||||||
K_C2, K_Db2, K_D2, K_Eb2, K_E2, K_F2, K_Gb2, K_G2,
|
|
||||||
|
constexpr double volume = 1800;
|
||||||
|
|
||||||
|
enum Switch {
|
||||||
|
Off,
|
||||||
|
On,
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool is_white(PianoKey n)
|
enum Direction {
|
||||||
{
|
Down,
|
||||||
switch (n) {
|
Up,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const double note_frequency[] = {
|
enum Wave {
|
||||||
/* 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,
|
Sine,
|
||||||
/* 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,
|
Triangle,
|
||||||
/* 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,
|
Square,
|
||||||
/* 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,
|
Saw,
|
||||||
/* 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,
|
Noise,
|
||||||
/* Octave 6 */ 1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53,
|
|
||||||
/* Octave 7 */ 2093.00, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520.00, 3729.31, 3951.07,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constexpr 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;
|
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) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||||
|
* Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* 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.
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "Music.h"
|
#include "AudioEngine.h"
|
||||||
#include "PianoWidget.h"
|
#include "MainWidget.h"
|
||||||
#include <LibAudio/AClientConnection.h>
|
#include <LibAudio/AClientConnection.h>
|
||||||
#include <LibCore/CFile.h>
|
#include <LibCore/CFile.h>
|
||||||
#include <LibDraw/PNGLoader.h>
|
#include <LibDraw/PNGLoader.h>
|
||||||
|
@ -39,34 +40,36 @@
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
GApplication app(argc, argv);
|
GApplication app(argc, argv);
|
||||||
|
|
||||||
auto audio_client = AClientConnection::construct();
|
auto audio_client = AClientConnection::construct();
|
||||||
audio_client->handshake();
|
audio_client->handshake();
|
||||||
|
|
||||||
|
AudioEngine audio_engine;
|
||||||
|
|
||||||
auto window = GWindow::construct();
|
auto window = GWindow::construct();
|
||||||
|
auto main_widget = MainWidget::construct(audio_engine);
|
||||||
|
window->set_main_widget(main_widget);
|
||||||
window->set_title("Piano");
|
window->set_title("Piano");
|
||||||
window->set_rect(100, 100, 512, 512);
|
window->set_rect(90, 90, 840, 600);
|
||||||
|
|
||||||
auto piano_widget = PianoWidget::construct();
|
|
||||||
window->set_main_widget(piano_widget);
|
|
||||||
window->show();
|
|
||||||
window->set_icon(load_png("/res/icons/16x16/app-piano.png"));
|
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");
|
auto audio = CFile::construct("/dev/audio");
|
||||||
if (!audio->open(CIODevice::WriteOnly)) {
|
if (!audio->open(CIODevice::WriteOnly)) {
|
||||||
dbgprintf("Can't open audio device: %s", audio->error_string());
|
dbgprintf("Can't open audio device: %s", audio->error_string());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FixedArray<Sample> buffer(sample_count);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
u8 buffer[4096];
|
audio_engine.fill_buffer(buffer);
|
||||||
piano_widget->fill_audio_buffer(buffer, sizeof(buffer));
|
audio->write(reinterpret_cast<u8*>(buffer.data()), buffer_size);
|
||||||
audio->write(buffer, sizeof(buffer));
|
CEventLoop::current().post_event(*main_widget, make<CCustomEvent>(0));
|
||||||
CEventLoop::current().post_event(*piano_widget, make<CCustomEvent>(0));
|
|
||||||
CEventLoop::wake();
|
CEventLoop::wake();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sound_thread.start();
|
audio_thread.start();
|
||||||
|
|
||||||
auto menubar = make<GMenuBar>();
|
auto menubar = make<GMenuBar>();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue