mirror of
https://github.com/RGBCube/serenity
synced 2025-07-28 13:17:34 +00:00
Applications: Move to Userland/Applications/
This commit is contained in:
parent
aa939c4b4b
commit
dc28c07fa5
287 changed files with 1 additions and 1 deletions
14
Userland/Applications/Piano/CMakeLists.txt
Normal file
14
Userland/Applications/Piano/CMakeLists.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
set(SOURCES
|
||||
Track.cpp
|
||||
TrackManager.cpp
|
||||
KeysWidget.cpp
|
||||
KnobsWidget.cpp
|
||||
main.cpp
|
||||
MainWidget.cpp
|
||||
RollWidget.cpp
|
||||
SamplerWidget.cpp
|
||||
WaveWidget.cpp
|
||||
)
|
||||
|
||||
serenity_app(Piano ICON app-piano)
|
||||
target_link_libraries(Piano LibAudio LibGUI)
|
328
Userland/Applications/Piano/KeysWidget.cpp
Normal file
328
Userland/Applications/Piano/KeysWidget.cpp
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
* 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 "TrackManager.h"
|
||||
#include <LibGUI/Painter.h>
|
||||
|
||||
KeysWidget::KeysWidget(TrackManager& track_manager)
|
||||
: m_track_manager(track_manager)
|
||||
{
|
||||
set_fill_with_background_color(true);
|
||||
}
|
||||
|
||||
KeysWidget::~KeysWidget()
|
||||
{
|
||||
}
|
||||
|
||||
int KeysWidget::mouse_note() const
|
||||
{
|
||||
if (m_mouse_down && m_mouse_note + m_track_manager.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_track_manager.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_track_manager.set_note_current_octave(key, switch_key);
|
||||
}
|
||||
|
||||
bool KeysWidget::note_is_set(int note) const
|
||||
{
|
||||
if (note < m_track_manager.octave_base())
|
||||
return false;
|
||||
|
||||
if (note >= m_track_manager.octave_base() + note_count)
|
||||
return false;
|
||||
|
||||
return m_key_on[note - m_track_manager.octave_base()] != 0;
|
||||
}
|
||||
|
||||
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(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Painter painter(*this);
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
|
||||
int note = 0;
|
||||
int x = 0;
|
||||
int i = 0;
|
||||
for (;;) {
|
||||
Gfx::IntRect 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), Gfx::TextAlignment::Center, Color::Black);
|
||||
}
|
||||
|
||||
note += white_key_note_accumulator[i % white_keys_per_octave];
|
||||
x += white_key_width;
|
||||
++i;
|
||||
|
||||
if (note + m_track_manager.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 (;;) {
|
||||
Gfx::IntRect 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), Gfx::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_track_manager.octave_base() >= note_count)
|
||||
break;
|
||||
if (x >= frame_inner_rect().width())
|
||||
break;
|
||||
}
|
||||
|
||||
GUI::Frame::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(const Gfx::IntPoint& a_point) const
|
||||
{
|
||||
if (!frame_inner_rect().contains(a_point))
|
||||
return -1;
|
||||
|
||||
auto point = a_point;
|
||||
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;
|
||||
Gfx::IntRect 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;
|
||||
Gfx::IntRect 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(GUI::MouseEvent& event)
|
||||
{
|
||||
if (event.button() != GUI::MouseButton::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(GUI::MouseEvent& event)
|
||||
{
|
||||
if (event.button() != GUI::MouseButton::Left)
|
||||
return;
|
||||
|
||||
m_mouse_down = false;
|
||||
|
||||
set_key(m_mouse_note, Off);
|
||||
update();
|
||||
}
|
||||
|
||||
void KeysWidget::mousemove_event(GUI::MouseEvent& 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;
|
||||
}
|
62
Userland/Applications/Piano/KeysWidget.h
Normal file
62
Userland/Applications/Piano/KeysWidget.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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/Frame.h>
|
||||
|
||||
class TrackManager;
|
||||
|
||||
class KeysWidget final : public GUI::Frame {
|
||||
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);
|
||||
bool note_is_set(int note) const;
|
||||
|
||||
private:
|
||||
explicit KeysWidget(TrackManager&);
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
virtual void mousedown_event(GUI::MouseEvent&) override;
|
||||
virtual void mouseup_event(GUI::MouseEvent&) override;
|
||||
virtual void mousemove_event(GUI::MouseEvent&) override;
|
||||
|
||||
int note_for_event_position(const Gfx::IntPoint&) const;
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
|
||||
u8 m_key_on[note_count] { 0 };
|
||||
|
||||
bool m_mouse_down { false };
|
||||
int m_mouse_note { -1 };
|
||||
};
|
183
Userland/Applications/Piano/KnobsWidget.cpp
Normal file
183
Userland/Applications/Piano/KnobsWidget.cpp
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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 "MainWidget.h"
|
||||
#include "TrackManager.h"
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/Slider.h>
|
||||
|
||||
constexpr int max_attack = 1000;
|
||||
constexpr int max_decay = 1000;
|
||||
constexpr int max_sustain = 1000;
|
||||
constexpr int max_release = 1000;
|
||||
constexpr int max_delay = 8;
|
||||
|
||||
KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget)
|
||||
: m_track_manager(track_manager)
|
||||
, m_main_widget(main_widget)
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
set_fill_with_background_color(true);
|
||||
|
||||
m_labels_container = add<GUI::Widget>();
|
||||
m_labels_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
m_labels_container->set_fixed_height(20);
|
||||
|
||||
m_octave_label = m_labels_container->add<GUI::Label>("Octave");
|
||||
m_wave_label = m_labels_container->add<GUI::Label>("Wave");
|
||||
m_attack_label = m_labels_container->add<GUI::Label>("Attack");
|
||||
m_decay_label = m_labels_container->add<GUI::Label>("Decay");
|
||||
m_sustain_label = m_labels_container->add<GUI::Label>("Sustain");
|
||||
m_release_label = m_labels_container->add<GUI::Label>("Release");
|
||||
m_delay_label = m_labels_container->add<GUI::Label>("Delay");
|
||||
|
||||
m_values_container = add<GUI::Widget>();
|
||||
m_values_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
m_values_container->set_fixed_height(10);
|
||||
|
||||
m_octave_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.octave()));
|
||||
m_wave_value = m_values_container->add<GUI::Label>(wave_strings[m_track_manager.current_track().wave()]);
|
||||
m_attack_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().attack()));
|
||||
m_decay_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().decay()));
|
||||
m_sustain_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().sustain()));
|
||||
m_release_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().release()));
|
||||
m_delay_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().delay()));
|
||||
|
||||
m_knobs_container = add<GUI::Widget>();
|
||||
m_knobs_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
// FIXME: Implement vertical flipping in GUI::Slider, not here.
|
||||
|
||||
m_octave_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
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((octave_max - 1) - (m_track_manager.octave() - 1));
|
||||
m_octave_knob->on_change = [this](int value) {
|
||||
int new_octave = octave_max - value;
|
||||
if (m_change_underlying)
|
||||
m_main_widget.set_octave_and_ensure_note_change(new_octave);
|
||||
ASSERT(new_octave == m_track_manager.octave());
|
||||
m_octave_value->set_text(String::number(new_octave));
|
||||
};
|
||||
|
||||
m_wave_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
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_track_manager.current_track().wave());
|
||||
m_wave_knob->on_change = [this](int value) {
|
||||
int new_wave = last_wave - value;
|
||||
if (m_change_underlying)
|
||||
m_track_manager.current_track().set_wave(new_wave);
|
||||
ASSERT(new_wave == m_track_manager.current_track().wave());
|
||||
m_wave_value->set_text(wave_strings[new_wave]);
|
||||
};
|
||||
|
||||
m_attack_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
m_attack_knob->set_range(0, max_attack);
|
||||
m_attack_knob->set_value(max_attack - m_track_manager.current_track().attack());
|
||||
m_attack_knob->set_step(100);
|
||||
m_attack_knob->on_change = [this](int value) {
|
||||
int new_attack = max_attack - value;
|
||||
if (m_change_underlying)
|
||||
m_track_manager.current_track().set_attack(new_attack);
|
||||
ASSERT(new_attack == m_track_manager.current_track().attack());
|
||||
m_attack_value->set_text(String::number(new_attack));
|
||||
};
|
||||
|
||||
m_decay_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
m_decay_knob->set_range(0, max_decay);
|
||||
m_decay_knob->set_value(max_decay - m_track_manager.current_track().decay());
|
||||
m_decay_knob->set_step(100);
|
||||
m_decay_knob->on_change = [this](int value) {
|
||||
int new_decay = max_decay - value;
|
||||
if (m_change_underlying)
|
||||
m_track_manager.current_track().set_decay(new_decay);
|
||||
ASSERT(new_decay == m_track_manager.current_track().decay());
|
||||
m_decay_value->set_text(String::number(new_decay));
|
||||
};
|
||||
|
||||
m_sustain_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
m_sustain_knob->set_range(0, max_sustain);
|
||||
m_sustain_knob->set_value(max_sustain - m_track_manager.current_track().sustain());
|
||||
m_sustain_knob->set_step(100);
|
||||
m_sustain_knob->on_change = [this](int value) {
|
||||
int new_sustain = max_sustain - value;
|
||||
if (m_change_underlying)
|
||||
m_track_manager.current_track().set_sustain(new_sustain);
|
||||
ASSERT(new_sustain == m_track_manager.current_track().sustain());
|
||||
m_sustain_value->set_text(String::number(new_sustain));
|
||||
};
|
||||
|
||||
m_release_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
m_release_knob->set_range(0, max_release);
|
||||
m_release_knob->set_value(max_release - m_track_manager.current_track().release());
|
||||
m_release_knob->set_step(100);
|
||||
m_release_knob->on_change = [this](int value) {
|
||||
int new_release = max_release - value;
|
||||
if (m_change_underlying)
|
||||
m_track_manager.current_track().set_release(new_release);
|
||||
ASSERT(new_release == m_track_manager.current_track().release());
|
||||
m_release_value->set_text(String::number(new_release));
|
||||
};
|
||||
|
||||
m_delay_knob = m_knobs_container->add<GUI::VerticalSlider>();
|
||||
m_delay_knob->set_range(0, max_delay);
|
||||
m_delay_knob->set_value(max_delay - m_track_manager.current_track().delay());
|
||||
m_delay_knob->on_change = [this](int value) {
|
||||
int new_delay = max_delay - value;
|
||||
if (m_change_underlying)
|
||||
m_track_manager.current_track().set_delay(new_delay);
|
||||
ASSERT(new_delay == m_track_manager.current_track().delay());
|
||||
m_delay_value->set_text(String::number(new_delay));
|
||||
};
|
||||
}
|
||||
|
||||
KnobsWidget::~KnobsWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void KnobsWidget::update_knobs()
|
||||
{
|
||||
m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave());
|
||||
|
||||
// FIXME: This is needed because when the slider is changed normally, we
|
||||
// need to change the underlying value, but if the keyboard was used, we
|
||||
// need to change the slider without changing the underlying value.
|
||||
m_change_underlying = false;
|
||||
|
||||
m_octave_knob->set_value(octave_max - m_track_manager.octave());
|
||||
m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave());
|
||||
m_attack_knob->set_value(max_attack - m_track_manager.current_track().attack());
|
||||
m_decay_knob->set_value(max_decay - m_track_manager.current_track().decay());
|
||||
m_sustain_knob->set_value(max_sustain - m_track_manager.current_track().sustain());
|
||||
m_release_knob->set_value(max_release - m_track_manager.current_track().release());
|
||||
m_delay_knob->set_value(max_delay - m_track_manager.current_track().delay());
|
||||
|
||||
m_change_underlying = true;
|
||||
}
|
76
Userland/Applications/Piano/KnobsWidget.h
Normal file
76
Userland/Applications/Piano/KnobsWidget.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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/Frame.h>
|
||||
|
||||
class TrackManager;
|
||||
class MainWidget;
|
||||
|
||||
class KnobsWidget final : public GUI::Frame {
|
||||
C_OBJECT(KnobsWidget)
|
||||
public:
|
||||
virtual ~KnobsWidget() override;
|
||||
|
||||
void update_knobs();
|
||||
|
||||
private:
|
||||
KnobsWidget(TrackManager&, MainWidget&);
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
MainWidget& m_main_widget;
|
||||
|
||||
RefPtr<GUI::Widget> m_labels_container;
|
||||
RefPtr<GUI::Label> m_octave_label;
|
||||
RefPtr<GUI::Label> m_wave_label;
|
||||
RefPtr<GUI::Label> m_attack_label;
|
||||
RefPtr<GUI::Label> m_decay_label;
|
||||
RefPtr<GUI::Label> m_sustain_label;
|
||||
RefPtr<GUI::Label> m_release_label;
|
||||
RefPtr<GUI::Label> m_delay_label;
|
||||
|
||||
RefPtr<GUI::Widget> m_values_container;
|
||||
RefPtr<GUI::Label> m_octave_value;
|
||||
RefPtr<GUI::Label> m_wave_value;
|
||||
RefPtr<GUI::Label> m_attack_value;
|
||||
RefPtr<GUI::Label> m_decay_value;
|
||||
RefPtr<GUI::Label> m_sustain_value;
|
||||
RefPtr<GUI::Label> m_release_value;
|
||||
RefPtr<GUI::Label> m_delay_value;
|
||||
|
||||
RefPtr<GUI::Widget> m_knobs_container;
|
||||
RefPtr<GUI::Slider> m_octave_knob;
|
||||
RefPtr<GUI::Slider> m_wave_knob;
|
||||
RefPtr<GUI::Slider> m_attack_knob;
|
||||
RefPtr<GUI::Slider> m_decay_knob;
|
||||
RefPtr<GUI::Slider> m_sustain_knob;
|
||||
RefPtr<GUI::Slider> m_release_knob;
|
||||
RefPtr<GUI::Slider> m_delay_knob;
|
||||
|
||||
bool m_change_underlying { true };
|
||||
};
|
179
Userland/Applications/Piano/MainWidget.cpp
Normal file
179
Userland/Applications/Piano/MainWidget.cpp
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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 "KeysWidget.h"
|
||||
#include "KnobsWidget.h"
|
||||
#include "RollWidget.h"
|
||||
#include "SamplerWidget.h"
|
||||
#include "TrackManager.h"
|
||||
#include "WaveWidget.h"
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/TabWidget.h>
|
||||
|
||||
MainWidget::MainWidget(TrackManager& track_manager)
|
||||
: m_track_manager(track_manager)
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
layout()->set_spacing(2);
|
||||
layout()->set_margins({ 2, 2, 2, 2 });
|
||||
set_fill_with_background_color(true);
|
||||
|
||||
m_wave_widget = add<WaveWidget>(track_manager);
|
||||
m_wave_widget->set_fixed_height(100);
|
||||
|
||||
m_tab_widget = add<GUI::TabWidget>();
|
||||
m_roll_widget = m_tab_widget->add_tab<RollWidget>("Piano Roll", track_manager);
|
||||
|
||||
m_roll_widget->set_fixed_height(300);
|
||||
|
||||
m_tab_widget->add_tab<SamplerWidget>("Sampler", track_manager);
|
||||
|
||||
m_keys_and_knobs_container = add<GUI::Widget>();
|
||||
m_keys_and_knobs_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
m_keys_and_knobs_container->layout()->set_spacing(2);
|
||||
m_keys_and_knobs_container->set_fixed_height(100);
|
||||
m_keys_and_knobs_container->set_fill_with_background_color(true);
|
||||
|
||||
m_keys_widget = m_keys_and_knobs_container->add<KeysWidget>(track_manager);
|
||||
|
||||
m_knobs_widget = m_keys_and_knobs_container->add<KnobsWidget>(track_manager, *this);
|
||||
m_knobs_widget->set_fixed_width(350);
|
||||
|
||||
m_roll_widget->set_keys_widget(m_keys_widget);
|
||||
}
|
||||
|
||||
MainWidget::~MainWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void MainWidget::add_actions(GUI::Menu& menu)
|
||||
{
|
||||
menu.add_action(GUI::Action::create("Add track", { Mod_Ctrl, Key_T }, [&](auto&) {
|
||||
m_track_manager.add_track();
|
||||
}));
|
||||
|
||||
menu.add_action(GUI::Action::create("Next track", { Mod_Ctrl, Key_N }, [&](auto&) {
|
||||
turn_off_pressed_keys();
|
||||
m_track_manager.next_track();
|
||||
turn_on_pressed_keys();
|
||||
|
||||
m_knobs_widget->update_knobs();
|
||||
}));
|
||||
}
|
||||
|
||||
// FIXME: There are some unnecessary calls to update() throughout this program,
|
||||
// which are an easy target for optimization.
|
||||
|
||||
void MainWidget::custom_event(Core::CustomEvent&)
|
||||
{
|
||||
m_wave_widget->update();
|
||||
m_roll_widget->update();
|
||||
}
|
||||
|
||||
void MainWidget::keydown_event(GUI::KeyEvent& 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(GUI::KeyEvent& 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_track_manager.current_track().set_wave(Up);
|
||||
m_knobs_widget->update_knobs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::turn_off_pressed_keys()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::turn_on_pressed_keys()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::set_octave_and_ensure_note_change(int octave)
|
||||
{
|
||||
turn_off_pressed_keys();
|
||||
m_track_manager.set_octave(octave);
|
||||
turn_on_pressed_keys();
|
||||
|
||||
m_knobs_widget->update_knobs();
|
||||
m_keys_widget->update();
|
||||
}
|
||||
|
||||
void MainWidget::set_octave_and_ensure_note_change(Direction direction)
|
||||
{
|
||||
turn_off_pressed_keys();
|
||||
m_track_manager.set_octave(direction);
|
||||
turn_on_pressed_keys();
|
||||
|
||||
m_knobs_widget->update_knobs();
|
||||
m_keys_widget->update();
|
||||
}
|
74
Userland/Applications/Piano/MainWidget.h
Normal file
74
Userland/Applications/Piano/MainWidget.h
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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/Widget.h>
|
||||
|
||||
class TrackManager;
|
||||
class WaveWidget;
|
||||
class RollWidget;
|
||||
class SamplerWidget;
|
||||
class KeysWidget;
|
||||
class KnobsWidget;
|
||||
|
||||
class MainWidget final : public GUI::Widget {
|
||||
C_OBJECT(MainWidget)
|
||||
public:
|
||||
virtual ~MainWidget() override;
|
||||
|
||||
void add_actions(GUI::Menu&);
|
||||
|
||||
void set_octave_and_ensure_note_change(Direction);
|
||||
void set_octave_and_ensure_note_change(int);
|
||||
|
||||
private:
|
||||
explicit MainWidget(TrackManager&);
|
||||
|
||||
virtual void keydown_event(GUI::KeyEvent&) override;
|
||||
virtual void keyup_event(GUI::KeyEvent&) override;
|
||||
virtual void custom_event(Core::CustomEvent&) override;
|
||||
|
||||
void note_key_action(int key_code, Switch);
|
||||
void special_key_action(int key_code);
|
||||
|
||||
void turn_off_pressed_keys();
|
||||
void turn_on_pressed_keys();
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
|
||||
RefPtr<WaveWidget> m_wave_widget;
|
||||
RefPtr<RollWidget> m_roll_widget;
|
||||
RefPtr<SamplerWidget> m_sampler_widget;
|
||||
RefPtr<GUI::TabWidget> m_tab_widget;
|
||||
RefPtr<GUI::Widget> m_keys_and_knobs_container;
|
||||
RefPtr<KeysWidget> m_keys_widget;
|
||||
RefPtr<KnobsWidget> m_knobs_widget;
|
||||
|
||||
bool m_keys_pressed[key_code_count] { false };
|
||||
};
|
329
Userland/Applications/Piano/Music.h
Normal file
329
Userland/Applications/Piano/Music.h
Normal file
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* 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 <AK/Types.h>
|
||||
#include <LibGfx/Color.h>
|
||||
|
||||
namespace Music {
|
||||
|
||||
// CD quality
|
||||
// - Stereo
|
||||
// - 16 bit
|
||||
// - 44,100 samples/sec
|
||||
// - 1,411.2 kbps
|
||||
|
||||
struct Sample {
|
||||
i16 left;
|
||||
i16 right;
|
||||
};
|
||||
|
||||
constexpr int sample_count = 1024;
|
||||
|
||||
constexpr int buffer_size = sample_count * sizeof(Sample);
|
||||
|
||||
constexpr double sample_rate = 44100;
|
||||
|
||||
constexpr double volume = 1800;
|
||||
|
||||
enum Switch {
|
||||
Off,
|
||||
On,
|
||||
};
|
||||
|
||||
struct RollNote {
|
||||
u32 length() const { return (off_sample - on_sample) + 1; }
|
||||
|
||||
u32 on_sample;
|
||||
u32 off_sample;
|
||||
};
|
||||
|
||||
enum Direction {
|
||||
Down,
|
||||
Up,
|
||||
};
|
||||
|
||||
enum Wave {
|
||||
Sine,
|
||||
Triangle,
|
||||
Square,
|
||||
Saw,
|
||||
Noise,
|
||||
RecordedSample,
|
||||
};
|
||||
|
||||
constexpr const char* wave_strings[] = {
|
||||
"Sine",
|
||||
"Triangle",
|
||||
"Square",
|
||||
"Saw",
|
||||
"Noise",
|
||||
"Sample",
|
||||
};
|
||||
|
||||
constexpr int first_wave = Sine;
|
||||
constexpr int last_wave = RecordedSample;
|
||||
|
||||
enum Envelope {
|
||||
Done,
|
||||
Attack,
|
||||
Decay,
|
||||
Release,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const Color left_wave_colors[] = {
|
||||
// Sine
|
||||
{
|
||||
255,
|
||||
192,
|
||||
0,
|
||||
},
|
||||
// Triangle
|
||||
{
|
||||
35,
|
||||
171,
|
||||
35,
|
||||
},
|
||||
// Square
|
||||
{
|
||||
128,
|
||||
160,
|
||||
255,
|
||||
},
|
||||
// Saw
|
||||
{
|
||||
240,
|
||||
100,
|
||||
128,
|
||||
},
|
||||
// Noise
|
||||
{
|
||||
197,
|
||||
214,
|
||||
225,
|
||||
},
|
||||
// RecordedSample
|
||||
{
|
||||
227,
|
||||
39,
|
||||
39,
|
||||
},
|
||||
};
|
||||
|
||||
const Color right_wave_colors[] = {
|
||||
// Sine
|
||||
{
|
||||
255,
|
||||
223,
|
||||
0,
|
||||
},
|
||||
// Triangle
|
||||
{
|
||||
35,
|
||||
171,
|
||||
90,
|
||||
},
|
||||
// Square
|
||||
{
|
||||
139,
|
||||
128,
|
||||
255,
|
||||
},
|
||||
// Saw
|
||||
{
|
||||
240,
|
||||
100,
|
||||
220,
|
||||
},
|
||||
// Noise
|
||||
{
|
||||
197,
|
||||
223,
|
||||
225,
|
||||
},
|
||||
// RecordedSample
|
||||
{
|
||||
227,
|
||||
105,
|
||||
39,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
constexpr double beats_per_minute = 60;
|
||||
constexpr int beats_per_bar = 4;
|
||||
constexpr int notes_per_beat = 4;
|
||||
constexpr int roll_length = (sample_rate / (beats_per_minute / 60)) * beats_per_bar;
|
||||
|
||||
constexpr const char* note_names[] = {
|
||||
"C",
|
||||
"C#",
|
||||
"D",
|
||||
"D#",
|
||||
"E",
|
||||
"F",
|
||||
"F#",
|
||||
"G",
|
||||
"G#",
|
||||
"A",
|
||||
"A#",
|
||||
"B",
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
constexpr double middle_c = note_frequencies[36];
|
||||
|
||||
}
|
||||
|
||||
using namespace Music;
|
248
Userland/Applications/Piano/RollWidget.cpp
Normal file
248
Userland/Applications/Piano/RollWidget.cpp
Normal file
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* 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 "TrackManager.h"
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGUI/ScrollBar.h>
|
||||
#include <LibGfx/Font.h>
|
||||
#include <LibGfx/FontDatabase.h>
|
||||
#include <math.h>
|
||||
|
||||
constexpr int note_height = 20;
|
||||
constexpr int max_note_width = note_height * 2;
|
||||
constexpr int roll_height = note_count * note_height;
|
||||
constexpr int horizontal_scroll_sensitivity = 20;
|
||||
constexpr int max_zoom = 1 << 8;
|
||||
|
||||
RollWidget::RollWidget(TrackManager& track_manager)
|
||||
: m_track_manager(track_manager)
|
||||
{
|
||||
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(GUI::PaintEvent& event)
|
||||
{
|
||||
m_roll_width = widget_inner_rect().width() * m_zoom_level;
|
||||
set_content_size({ m_roll_width, roll_height });
|
||||
|
||||
// Divide the roll by the maximum note width. If we get fewer notes than
|
||||
// our time signature requires, round up. Otherwise, round down to the
|
||||
// nearest x*(2^y), where x is the base number of notes of our time
|
||||
// signature. In other words, find a number that is a double of our time
|
||||
// signature. For 4/4 that would be 16, 32, 64, 128 ...
|
||||
m_num_notes = m_roll_width / max_note_width;
|
||||
int time_signature_notes = beats_per_bar * notes_per_beat;
|
||||
if (m_num_notes < time_signature_notes)
|
||||
m_num_notes = time_signature_notes;
|
||||
else
|
||||
m_num_notes = time_signature_notes * pow(2, static_cast<int>(log2(m_num_notes / time_signature_notes)));
|
||||
m_note_width = static_cast<double>(m_roll_width) / m_num_notes;
|
||||
|
||||
// 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);
|
||||
|
||||
int x_offset = horizontal_scrollbar().value();
|
||||
int horizontal_note_offset_remainder = fmod(x_offset, m_note_width);
|
||||
int horizontal_paint_area = widget_inner_rect().width() + horizontal_note_offset_remainder;
|
||||
if (fmod(horizontal_paint_area, m_note_width) != 0)
|
||||
horizontal_paint_area += m_note_width;
|
||||
int horizontal_notes_to_paint = horizontal_paint_area / m_note_width;
|
||||
|
||||
GUI::Painter painter(*this);
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.translate(-horizontal_note_offset_remainder, -note_offset_remainder);
|
||||
|
||||
for (int y = 0; y < notes_to_paint; ++y) {
|
||||
int y_pos = y * note_height;
|
||||
|
||||
int note = (note_count - note_offset - 1) - y;
|
||||
for (int x = 0; x < horizontal_notes_to_paint; ++x) {
|
||||
// This is needed to avoid rounding errors. You can't just use
|
||||
// m_note_width as the width.
|
||||
int x_pos = x * m_note_width;
|
||||
int next_x_pos = (x + 1) * m_note_width;
|
||||
int distance_to_next_x = next_x_pos - x_pos;
|
||||
Gfx::IntRect rect(x_pos, y_pos, distance_to_next_x, note_height);
|
||||
|
||||
if (key_pattern[key_pattern_index] == Black)
|
||||
painter.fill_rect(rect, Color::LightGray);
|
||||
else
|
||||
painter.fill_rect(rect, Color::White);
|
||||
|
||||
if (keys_widget() && keys_widget()->note_is_set(note))
|
||||
painter.fill_rect(rect, note_pressed_color.with_alpha(128));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
painter.translate(-x_offset, -y_offset);
|
||||
painter.translate(horizontal_note_offset_remainder, note_offset_remainder);
|
||||
|
||||
for (int note = note_count - (note_offset + notes_to_paint); note <= (note_count - 1) - note_offset; ++note) {
|
||||
int y = ((note_count - 1) - note) * note_height;
|
||||
for (auto roll_note : m_track_manager.current_track().roll_notes(note)) {
|
||||
int x = m_roll_width * (static_cast<double>(roll_note.on_sample) / roll_length);
|
||||
int width = m_roll_width * (static_cast<double>(roll_note.length()) / roll_length);
|
||||
if (x + width < x_offset || x > x_offset + widget_inner_rect().width())
|
||||
continue;
|
||||
if (width < 2)
|
||||
width = 2;
|
||||
|
||||
int height = note_height;
|
||||
|
||||
Gfx::IntRect rect(x, y, width, height);
|
||||
painter.fill_rect(rect, note_pressed_color);
|
||||
painter.draw_rect(rect, Color::Black);
|
||||
}
|
||||
Gfx::IntRect note_name_rect(3, y, 1, note_height);
|
||||
const char* note_name = note_names[note % notes_per_octave];
|
||||
|
||||
painter.draw_text(note_name_rect, note_name, Gfx::TextAlignment::CenterLeft);
|
||||
note_name_rect.move_by(Gfx::FontDatabase::default_font().width(note_name) + 2, 0);
|
||||
if (note % notes_per_octave == 0)
|
||||
painter.draw_text(note_name_rect, String::formatted("{}", note / notes_per_octave + 1), Gfx::TextAlignment::CenterLeft);
|
||||
}
|
||||
|
||||
int x = m_roll_width * (static_cast<double>(m_track_manager.time()) / roll_length);
|
||||
if (x > x_offset && x <= x_offset + widget_inner_rect().width())
|
||||
painter.draw_line({ x, 0 }, { x, roll_height }, Gfx::Color::Black);
|
||||
|
||||
GUI::Frame::paint_event(event);
|
||||
}
|
||||
|
||||
void RollWidget::mousedown_event(GUI::MouseEvent& event)
|
||||
{
|
||||
if (!widget_inner_rect().contains(event.x(), event.y()))
|
||||
return;
|
||||
|
||||
m_note_drag_start = event.position();
|
||||
|
||||
int y = (m_note_drag_start.value().y() + vertical_scrollbar().value()) - frame_thickness();
|
||||
y /= note_height;
|
||||
m_drag_note = (note_count - 1) - y;
|
||||
|
||||
mousemove_event(event);
|
||||
}
|
||||
|
||||
void RollWidget::mousemove_event(GUI::MouseEvent& event)
|
||||
{
|
||||
if (!m_note_drag_start.has_value())
|
||||
return;
|
||||
|
||||
if (m_note_drag_location.has_value()) {
|
||||
// Clear previous note
|
||||
m_track_manager.current_track().set_roll_note(m_drag_note, m_note_drag_location.value().on_sample, m_note_drag_location.value().off_sample);
|
||||
}
|
||||
|
||||
auto get_note_x = [&](int x0) {
|
||||
// There's a case where we can't just use x / m_note_width. For example, if
|
||||
// your m_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 m_note_width is
|
||||
// fractional, being careful not to shift out of bounds.
|
||||
int x = (x0 + horizontal_scrollbar().value()) - frame_thickness();
|
||||
bool note_width_is_fractional = m_note_width - static_cast<int>(m_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 /= m_note_width;
|
||||
return x;
|
||||
};
|
||||
|
||||
int x0 = get_note_x(m_note_drag_start.value().x());
|
||||
int x1 = get_note_x(event.x());
|
||||
|
||||
u32 on_sample = roll_length * (static_cast<double>(min(x0, x1)) / m_num_notes);
|
||||
u32 off_sample = (roll_length * (static_cast<double>(max(x0, x1) + 1) / m_num_notes)) - 1;
|
||||
m_track_manager.current_track().set_roll_note(m_drag_note, on_sample, off_sample);
|
||||
m_note_drag_location = RollNote({ on_sample, off_sample });
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void RollWidget::mouseup_event([[maybe_unused]] GUI::MouseEvent& event)
|
||||
{
|
||||
m_note_drag_start = {};
|
||||
m_note_drag_location = {};
|
||||
}
|
||||
|
||||
// FIXME: Implement zoom and horizontal scroll events in LibGUI, not here.
|
||||
void RollWidget::mousewheel_event(GUI::MouseEvent& event)
|
||||
{
|
||||
if (event.modifiers() & KeyModifier::Mod_Shift) {
|
||||
horizontal_scrollbar().set_value(horizontal_scrollbar().value() + (event.wheel_delta() * horizontal_scroll_sensitivity));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(event.modifiers() & KeyModifier::Mod_Ctrl)) {
|
||||
GUI::ScrollableWidget::mousewheel_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
double multiplier = event.wheel_delta() >= 0 ? 0.5 : 2;
|
||||
|
||||
if (m_zoom_level * multiplier > max_zoom)
|
||||
return;
|
||||
|
||||
if (m_zoom_level * multiplier < 1) {
|
||||
if (m_zoom_level == 1)
|
||||
return;
|
||||
m_zoom_level = 1;
|
||||
} else {
|
||||
m_zoom_level *= multiplier;
|
||||
}
|
||||
|
||||
int absolute_x_of_pixel_at_cursor = horizontal_scrollbar().value() + event.position().x();
|
||||
int absolute_x_of_pixel_at_cursor_after_resize = absolute_x_of_pixel_at_cursor * multiplier;
|
||||
int new_scrollbar = absolute_x_of_pixel_at_cursor_after_resize - event.position().x();
|
||||
|
||||
m_roll_width = widget_inner_rect().width() * m_zoom_level;
|
||||
set_content_size({ m_roll_width, roll_height });
|
||||
|
||||
horizontal_scrollbar().set_value(new_scrollbar);
|
||||
}
|
64
Userland/Applications/Piano/RollWidget.h
Normal file
64
Userland/Applications/Piano/RollWidget.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 "KeysWidget.h"
|
||||
#include "Music.h"
|
||||
#include <LibGUI/ScrollableWidget.h>
|
||||
|
||||
class TrackManager;
|
||||
|
||||
class RollWidget final : public GUI::ScrollableWidget {
|
||||
C_OBJECT(RollWidget)
|
||||
public:
|
||||
virtual ~RollWidget() override;
|
||||
|
||||
const KeysWidget* keys_widget() const { return m_keys_widget; }
|
||||
void set_keys_widget(const KeysWidget* widget) { m_keys_widget = widget; }
|
||||
|
||||
private:
|
||||
explicit RollWidget(TrackManager&);
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
virtual void mousedown_event(GUI::MouseEvent& event) override;
|
||||
virtual void mousemove_event(GUI::MouseEvent& event) override;
|
||||
virtual void mouseup_event(GUI::MouseEvent& event) override;
|
||||
virtual void mousewheel_event(GUI::MouseEvent&) override;
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
const KeysWidget* m_keys_widget;
|
||||
|
||||
int m_roll_width { 0 };
|
||||
int m_num_notes { 0 };
|
||||
double m_note_width { 0.0 };
|
||||
int m_zoom_level { 1 };
|
||||
|
||||
Optional<Gfx::IntPoint> m_note_drag_start;
|
||||
Optional<RollNote> m_note_drag_location;
|
||||
int m_drag_note;
|
||||
};
|
130
Userland/Applications/Piano/SamplerWidget.cpp
Normal file
130
Userland/Applications/Piano/SamplerWidget.cpp
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright (c) 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 "SamplerWidget.h"
|
||||
#include "TrackManager.h"
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/FilePicker.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
|
||||
WaveEditor::WaveEditor(TrackManager& track_manager)
|
||||
: m_track_manager(track_manager)
|
||||
{
|
||||
}
|
||||
|
||||
WaveEditor::~WaveEditor()
|
||||
{
|
||||
}
|
||||
|
||||
int WaveEditor::sample_to_y(double percentage) const
|
||||
{
|
||||
double portion_of_half_height = percentage * ((frame_inner_rect().height() - 1) / 2.0);
|
||||
double y = (frame_inner_rect().height() / 2.0) + portion_of_half_height;
|
||||
return y;
|
||||
}
|
||||
|
||||
void WaveEditor::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Frame::paint_event(event);
|
||||
|
||||
GUI::Painter painter(*this);
|
||||
painter.fill_rect(frame_inner_rect(), Color::Black);
|
||||
|
||||
auto recorded_sample = m_track_manager.current_track().recorded_sample();
|
||||
if (recorded_sample.is_empty())
|
||||
return;
|
||||
|
||||
double width_scale = static_cast<double>(frame_inner_rect().width()) / recorded_sample.size();
|
||||
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
|
||||
int prev_x = 0;
|
||||
int left_prev_y = sample_to_y(recorded_sample[0].left);
|
||||
int right_prev_y = sample_to_y(recorded_sample[0].right);
|
||||
painter.set_pixel({ prev_x, left_prev_y }, left_wave_colors[RecordedSample]);
|
||||
painter.set_pixel({ prev_x, right_prev_y }, right_wave_colors[RecordedSample]);
|
||||
|
||||
for (size_t x = 1; x < recorded_sample.size(); ++x) {
|
||||
int left_y = sample_to_y(recorded_sample[x].left);
|
||||
int right_y = sample_to_y(recorded_sample[x].right);
|
||||
|
||||
Gfx::IntPoint left_point1(prev_x * width_scale, left_prev_y);
|
||||
Gfx::IntPoint left_point2(x * width_scale, left_y);
|
||||
painter.draw_line(left_point1, left_point2, left_wave_colors[RecordedSample]);
|
||||
|
||||
Gfx::IntPoint right_point1(prev_x * width_scale, right_prev_y);
|
||||
Gfx::IntPoint right_point2(x * width_scale, right_y);
|
||||
painter.draw_line(right_point1, right_point2, right_wave_colors[RecordedSample]);
|
||||
|
||||
prev_x = x;
|
||||
left_prev_y = left_y;
|
||||
right_prev_y = right_y;
|
||||
}
|
||||
}
|
||||
|
||||
SamplerWidget::SamplerWidget(TrackManager& track_manager)
|
||||
: m_track_manager(track_manager)
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
layout()->set_margins({ 10, 10, 10, 10 });
|
||||
layout()->set_spacing(10);
|
||||
set_fill_with_background_color(true);
|
||||
|
||||
m_open_button_and_recorded_sample_name_container = add<GUI::Widget>();
|
||||
m_open_button_and_recorded_sample_name_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
m_open_button_and_recorded_sample_name_container->layout()->set_spacing(10);
|
||||
m_open_button_and_recorded_sample_name_container->set_fixed_height(24);
|
||||
|
||||
m_open_button = m_open_button_and_recorded_sample_name_container->add<GUI::Button>();
|
||||
m_open_button->set_fixed_size(24, 24);
|
||||
m_open_button->set_focus_policy(GUI::FocusPolicy::TabFocus);
|
||||
m_open_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"));
|
||||
m_open_button->on_click = [this](auto) {
|
||||
Optional<String> open_path = GUI::FilePicker::get_open_filepath(window());
|
||||
if (!open_path.has_value())
|
||||
return;
|
||||
String error_string = m_track_manager.current_track().set_recorded_sample(open_path.value());
|
||||
if (!error_string.is_empty()) {
|
||||
GUI::MessageBox::show(window(), String::formatted("Failed to load WAV file: {}", error_string.characters()), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
m_recorded_sample_name->set_text(open_path.value());
|
||||
m_wave_editor->update();
|
||||
};
|
||||
|
||||
m_recorded_sample_name = m_open_button_and_recorded_sample_name_container->add<GUI::Label>("No sample loaded");
|
||||
m_recorded_sample_name->set_text_alignment(Gfx::TextAlignment::CenterLeft);
|
||||
|
||||
m_wave_editor = add<WaveEditor>(m_track_manager);
|
||||
m_wave_editor->set_fixed_height(100);
|
||||
}
|
||||
|
||||
SamplerWidget::~SamplerWidget()
|
||||
{
|
||||
}
|
62
Userland/Applications/Piano/SamplerWidget.h
Normal file
62
Userland/Applications/Piano/SamplerWidget.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (c) 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/Frame.h>
|
||||
|
||||
class TrackManager;
|
||||
|
||||
class WaveEditor final : public GUI::Frame {
|
||||
C_OBJECT(WaveEditor)
|
||||
public:
|
||||
virtual ~WaveEditor() override;
|
||||
|
||||
private:
|
||||
explicit WaveEditor(TrackManager&);
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
|
||||
int sample_to_y(double percentage) const;
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
};
|
||||
|
||||
class SamplerWidget final : public GUI::Frame {
|
||||
C_OBJECT(SamplerWidget)
|
||||
public:
|
||||
virtual ~SamplerWidget() override;
|
||||
|
||||
private:
|
||||
explicit SamplerWidget(TrackManager&);
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
|
||||
RefPtr<GUI::Widget> m_open_button_and_recorded_sample_name_container;
|
||||
RefPtr<GUI::Button> m_open_button;
|
||||
RefPtr<GUI::Label> m_recorded_sample_name;
|
||||
RefPtr<WaveEditor> m_wave_editor;
|
||||
};
|
369
Userland/Applications/Piano/Track.cpp
Normal file
369
Userland/Applications/Piano/Track.cpp
Normal file
|
@ -0,0 +1,369 @@
|
|||
/*
|
||||
* 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 "Track.h"
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <LibAudio/Loader.h>
|
||||
#include <math.h>
|
||||
|
||||
Track::Track(const u32& time)
|
||||
: m_time(time)
|
||||
{
|
||||
set_sustain_impl(1000);
|
||||
set_attack(5);
|
||||
set_decay(1000);
|
||||
set_release(5);
|
||||
}
|
||||
|
||||
Track::~Track()
|
||||
{
|
||||
}
|
||||
|
||||
void Track::fill_sample(Sample& sample)
|
||||
{
|
||||
Audio::Sample new_sample;
|
||||
|
||||
for (size_t note = 0; note < note_count; ++note) {
|
||||
if (!m_roll_iters[note].is_end()) {
|
||||
if (m_roll_iters[note]->on_sample == m_time) {
|
||||
set_note(note, On);
|
||||
} else if (m_roll_iters[note]->off_sample == m_time) {
|
||||
set_note(note, Off);
|
||||
++m_roll_iters[note];
|
||||
if (m_roll_iters[note].is_end())
|
||||
m_roll_iters[note] = m_roll_notes[note].begin();
|
||||
}
|
||||
}
|
||||
|
||||
switch (m_envelope[note]) {
|
||||
case Done:
|
||||
continue;
|
||||
case Attack:
|
||||
m_power[note] += m_attack_step;
|
||||
if (m_power[note] >= 1) {
|
||||
m_power[note] = 1;
|
||||
m_envelope[note] = Decay;
|
||||
}
|
||||
break;
|
||||
case Decay:
|
||||
m_power[note] -= m_decay_step;
|
||||
if (m_power[note] < m_sustain_level)
|
||||
m_power[note] = m_sustain_level;
|
||||
break;
|
||||
case Release:
|
||||
m_power[note] -= m_release_step[note];
|
||||
if (m_power[note] <= 0) {
|
||||
m_power[note] = 0;
|
||||
m_envelope[note] = Done;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
Audio::Sample note_sample;
|
||||
switch (m_wave) {
|
||||
case Wave::Sine:
|
||||
note_sample = sine(note);
|
||||
break;
|
||||
case Wave::Saw:
|
||||
note_sample = saw(note);
|
||||
break;
|
||||
case Wave::Square:
|
||||
note_sample = square(note);
|
||||
break;
|
||||
case Wave::Triangle:
|
||||
note_sample = triangle(note);
|
||||
break;
|
||||
case Wave::Noise:
|
||||
note_sample = noise();
|
||||
break;
|
||||
case Wave::RecordedSample:
|
||||
note_sample = recorded_sample(note);
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
new_sample.left += note_sample.left * m_power[note] * volume;
|
||||
new_sample.right += note_sample.right * m_power[note] * volume;
|
||||
}
|
||||
|
||||
if (m_delay) {
|
||||
new_sample.left += m_delay_buffer[m_delay_index].left * 0.333333;
|
||||
new_sample.right += m_delay_buffer[m_delay_index].right * 0.333333;
|
||||
m_delay_buffer[m_delay_index].left = new_sample.left;
|
||||
m_delay_buffer[m_delay_index].right = new_sample.right;
|
||||
if (++m_delay_index >= m_delay_samples)
|
||||
m_delay_index = 0;
|
||||
}
|
||||
|
||||
sample.left += new_sample.left;
|
||||
sample.right += new_sample.right;
|
||||
}
|
||||
|
||||
void Track::reset()
|
||||
{
|
||||
memset(m_delay_buffer.data(), 0, m_delay_buffer.size() * sizeof(Sample));
|
||||
m_delay_index = 0;
|
||||
|
||||
memset(m_note_on, 0, sizeof(m_note_on));
|
||||
memset(m_power, 0, sizeof(m_power));
|
||||
memset(m_envelope, 0, sizeof(m_envelope));
|
||||
}
|
||||
|
||||
String Track::set_recorded_sample(const StringView& path)
|
||||
{
|
||||
NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
|
||||
if (loader->has_error())
|
||||
return String(loader->error_string());
|
||||
auto buffer = loader->get_more_samples(60 * sample_rate * sizeof(Sample)); // 1 minute maximum
|
||||
|
||||
if (!m_recorded_sample.is_empty())
|
||||
m_recorded_sample.clear();
|
||||
m_recorded_sample.resize(buffer->sample_count());
|
||||
|
||||
double peak = 0;
|
||||
for (int i = 0; i < buffer->sample_count(); ++i) {
|
||||
double left_abs = fabs(buffer->samples()[i].left);
|
||||
double right_abs = fabs(buffer->samples()[i].right);
|
||||
if (left_abs > peak)
|
||||
peak = left_abs;
|
||||
if (right_abs > peak)
|
||||
peak = right_abs;
|
||||
}
|
||||
|
||||
if (peak) {
|
||||
for (int i = 0; i < buffer->sample_count(); ++i) {
|
||||
m_recorded_sample[i].left = buffer->samples()[i].left / peak;
|
||||
m_recorded_sample[i].right = buffer->samples()[i].right / peak;
|
||||
}
|
||||
}
|
||||
|
||||
return String::empty();
|
||||
}
|
||||
|
||||
// All of the information for these waves is on Wikipedia.
|
||||
|
||||
Audio::Sample Track::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;
|
||||
}
|
||||
|
||||
Audio::Sample Track::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;
|
||||
}
|
||||
|
||||
Audio::Sample Track::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;
|
||||
}
|
||||
|
||||
Audio::Sample Track::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;
|
||||
}
|
||||
|
||||
Audio::Sample Track::noise() const
|
||||
{
|
||||
double random_percentage = static_cast<double>(rand()) / RAND_MAX;
|
||||
double w = (random_percentage * 2) - 1;
|
||||
return w;
|
||||
}
|
||||
|
||||
Audio::Sample Track::recorded_sample(size_t note)
|
||||
{
|
||||
int t = m_pos[note];
|
||||
if (t >= static_cast<int>(m_recorded_sample.size()))
|
||||
return 0;
|
||||
double w_left = m_recorded_sample[t].left;
|
||||
double w_right = m_recorded_sample[t].right;
|
||||
if (t + 1 < static_cast<int>(m_recorded_sample.size())) {
|
||||
double t_fraction = m_pos[note] - t;
|
||||
w_left += (m_recorded_sample[t + 1].left - m_recorded_sample[t].left) * t_fraction;
|
||||
w_right += (m_recorded_sample[t + 1].right - m_recorded_sample[t].right) * t_fraction;
|
||||
}
|
||||
double recorded_sample_step = note_frequencies[note] / middle_c;
|
||||
m_pos[note] += recorded_sample_step;
|
||||
return { w_left, w_right };
|
||||
}
|
||||
|
||||
static inline double calculate_step(double distance, int milliseconds)
|
||||
{
|
||||
if (milliseconds == 0)
|
||||
return distance;
|
||||
|
||||
constexpr double samples_per_millisecond = sample_rate / 1000.0;
|
||||
double samples = milliseconds * samples_per_millisecond;
|
||||
double step = distance / samples;
|
||||
return step;
|
||||
}
|
||||
|
||||
void Track::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_envelope[note] = Attack;
|
||||
}
|
||||
++m_note_on[note];
|
||||
} else {
|
||||
if (m_note_on[note] >= 1) {
|
||||
if (m_note_on[note] == 1) {
|
||||
m_release_step[note] = calculate_step(m_power[note], m_release);
|
||||
m_envelope[note] = Release;
|
||||
}
|
||||
--m_note_on[note];
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT(m_note_on[note] != NumericLimits<u8>::max());
|
||||
ASSERT(m_power[note] >= 0);
|
||||
}
|
||||
|
||||
void Track::sync_roll(int note)
|
||||
{
|
||||
auto it = m_roll_notes[note].find_if([&](auto& roll_note) { return roll_note.off_sample > m_time; });
|
||||
if (it.is_end())
|
||||
m_roll_iters[note] = m_roll_notes[note].begin();
|
||||
else
|
||||
m_roll_iters[note] = it;
|
||||
}
|
||||
|
||||
void Track::set_roll_note(int note, u32 on_sample, u32 off_sample)
|
||||
{
|
||||
RollNote new_roll_note = { on_sample, off_sample };
|
||||
|
||||
ASSERT(note >= 0 && note < note_count);
|
||||
ASSERT(new_roll_note.off_sample < roll_length);
|
||||
ASSERT(new_roll_note.length() >= 2);
|
||||
|
||||
for (auto it = m_roll_notes[note].begin(); !it.is_end();) {
|
||||
if (it->on_sample > new_roll_note.off_sample) {
|
||||
m_roll_notes[note].insert_before(it, new_roll_note);
|
||||
sync_roll(note);
|
||||
return;
|
||||
}
|
||||
if (it->on_sample <= new_roll_note.on_sample && it->off_sample >= new_roll_note.on_sample) {
|
||||
if (m_time >= it->on_sample && m_time <= it->off_sample)
|
||||
set_note(note, Off);
|
||||
m_roll_notes[note].remove(it);
|
||||
sync_roll(note);
|
||||
return;
|
||||
}
|
||||
if ((new_roll_note.on_sample == 0 || it->on_sample >= new_roll_note.on_sample - 1) && it->on_sample <= new_roll_note.off_sample) {
|
||||
if (m_time >= new_roll_note.off_sample && m_time <= it->off_sample)
|
||||
set_note(note, Off);
|
||||
m_roll_notes[note].remove(it);
|
||||
it = m_roll_notes[note].begin();
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
m_roll_notes[note].append(new_roll_note);
|
||||
sync_roll(note);
|
||||
}
|
||||
|
||||
void Track::set_wave(int wave)
|
||||
{
|
||||
ASSERT(wave >= first_wave && wave <= last_wave);
|
||||
m_wave = wave;
|
||||
}
|
||||
|
||||
void Track::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 Track::set_attack(int attack)
|
||||
{
|
||||
ASSERT(attack >= 0);
|
||||
m_attack = attack;
|
||||
m_attack_step = calculate_step(1, m_attack);
|
||||
}
|
||||
|
||||
void Track::set_decay(int decay)
|
||||
{
|
||||
ASSERT(decay >= 0);
|
||||
m_decay = decay;
|
||||
m_decay_step = calculate_step(1 - m_sustain_level, m_decay);
|
||||
}
|
||||
|
||||
void Track::set_sustain_impl(int sustain)
|
||||
{
|
||||
ASSERT(sustain >= 0);
|
||||
m_sustain = sustain;
|
||||
m_sustain_level = sustain / 1000.0;
|
||||
}
|
||||
|
||||
void Track::set_sustain(int sustain)
|
||||
{
|
||||
set_sustain_impl(sustain);
|
||||
set_decay(m_decay);
|
||||
}
|
||||
|
||||
void Track::set_release(int release)
|
||||
{
|
||||
ASSERT(release >= 0);
|
||||
m_release = release;
|
||||
}
|
||||
|
||||
void Track::set_delay(int delay)
|
||||
{
|
||||
ASSERT(delay >= 0);
|
||||
m_delay = delay;
|
||||
m_delay_samples = m_delay == 0 ? 0 : (sample_rate / (beats_per_minute / 60)) / m_delay;
|
||||
m_delay_buffer.resize(m_delay_samples);
|
||||
memset(m_delay_buffer.data(), 0, m_delay_buffer.size() * sizeof(Sample));
|
||||
m_delay_index = 0;
|
||||
}
|
104
Userland/Applications/Piano/Track.h
Normal file
104
Userland/Applications/Piano/Track.h
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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/Noncopyable.h>
|
||||
#include <AK/SinglyLinkedList.h>
|
||||
#include <LibAudio/Buffer.h>
|
||||
|
||||
typedef AK::SinglyLinkedListIterator<SinglyLinkedList<RollNote>, RollNote> RollIter;
|
||||
|
||||
class Track {
|
||||
AK_MAKE_NONCOPYABLE(Track);
|
||||
AK_MAKE_NONMOVABLE(Track);
|
||||
|
||||
public:
|
||||
explicit Track(const u32& time);
|
||||
~Track();
|
||||
|
||||
const Vector<Audio::Sample>& recorded_sample() const { return m_recorded_sample; }
|
||||
const SinglyLinkedList<RollNote>& roll_notes(int note) const { return m_roll_notes[note]; }
|
||||
int wave() const { return m_wave; }
|
||||
int attack() const { return m_attack; }
|
||||
int decay() const { return m_decay; }
|
||||
int sustain() const { return m_sustain; }
|
||||
int release() const { return m_release; }
|
||||
int delay() const { return m_delay; }
|
||||
|
||||
void fill_sample(Sample& sample);
|
||||
void reset();
|
||||
String set_recorded_sample(const StringView& path);
|
||||
void set_note(int note, Switch);
|
||||
void set_roll_note(int note, u32 on_sample, u32 off_sample);
|
||||
void set_wave(int wave);
|
||||
void set_wave(Direction);
|
||||
void set_attack(int attack);
|
||||
void set_decay(int decay);
|
||||
void set_sustain(int sustain);
|
||||
void set_release(int release);
|
||||
void set_delay(int delay);
|
||||
|
||||
private:
|
||||
Audio::Sample sine(size_t note);
|
||||
Audio::Sample saw(size_t note);
|
||||
Audio::Sample square(size_t note);
|
||||
Audio::Sample triangle(size_t note);
|
||||
Audio::Sample noise() const;
|
||||
Audio::Sample recorded_sample(size_t note);
|
||||
|
||||
void sync_roll(int note);
|
||||
void set_sustain_impl(int sustain);
|
||||
|
||||
Vector<Sample> m_delay_buffer;
|
||||
|
||||
Vector<Audio::Sample> m_recorded_sample;
|
||||
|
||||
u8 m_note_on[note_count] { 0 };
|
||||
double m_power[note_count] { 0 };
|
||||
double m_pos[note_count]; // Initialized lazily.
|
||||
Envelope m_envelope[note_count] { Done };
|
||||
|
||||
int m_wave { first_wave };
|
||||
int m_attack;
|
||||
double m_attack_step;
|
||||
int m_decay;
|
||||
double m_decay_step;
|
||||
int m_sustain;
|
||||
double m_sustain_level;
|
||||
int m_release;
|
||||
double m_release_step[note_count];
|
||||
int m_delay { 0 };
|
||||
size_t m_delay_samples { 0 };
|
||||
size_t m_delay_index { 0 };
|
||||
|
||||
const u32& m_time;
|
||||
|
||||
SinglyLinkedList<RollNote> m_roll_notes[note_count];
|
||||
RollIter m_roll_iters[note_count];
|
||||
};
|
104
Userland/Applications/Piano/TrackManager.cpp
Normal file
104
Userland/Applications/Piano/TrackManager.cpp
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 "TrackManager.h"
|
||||
|
||||
TrackManager::TrackManager()
|
||||
{
|
||||
add_track();
|
||||
}
|
||||
|
||||
TrackManager::~TrackManager()
|
||||
{
|
||||
}
|
||||
|
||||
void TrackManager::fill_buffer(Span<Sample> buffer)
|
||||
{
|
||||
memset(buffer.data(), 0, buffer_size);
|
||||
|
||||
for (size_t i = 0; i < buffer.size(); ++i) {
|
||||
for (auto& track : m_tracks)
|
||||
track->fill_sample(buffer[i]);
|
||||
|
||||
if (++m_time >= roll_length) {
|
||||
m_time = 0;
|
||||
if (!m_should_loop)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(m_current_back_buffer.data(), buffer.data(), buffer_size);
|
||||
swap(m_current_front_buffer, m_current_back_buffer);
|
||||
}
|
||||
|
||||
void TrackManager::reset()
|
||||
{
|
||||
memset(m_front_buffer.data(), 0, buffer_size);
|
||||
memset(m_back_buffer.data(), 0, buffer_size);
|
||||
|
||||
m_current_front_buffer = m_front_buffer.span();
|
||||
m_current_back_buffer = m_back_buffer.span();
|
||||
|
||||
m_time = 0;
|
||||
|
||||
for (auto& track : m_tracks)
|
||||
track->reset();
|
||||
}
|
||||
|
||||
void TrackManager::set_note_current_octave(int note, Switch switch_note)
|
||||
{
|
||||
current_track().set_note(note + octave_base(), switch_note);
|
||||
}
|
||||
|
||||
void TrackManager::set_octave(Direction direction)
|
||||
{
|
||||
if (direction == Up) {
|
||||
if (m_octave < octave_max)
|
||||
++m_octave;
|
||||
} else {
|
||||
if (m_octave > octave_min)
|
||||
--m_octave;
|
||||
}
|
||||
}
|
||||
|
||||
void TrackManager::set_octave(int octave)
|
||||
{
|
||||
if (octave <= octave_max && octave >= octave_min) {
|
||||
m_octave = octave;
|
||||
}
|
||||
}
|
||||
|
||||
void TrackManager::add_track()
|
||||
{
|
||||
m_tracks.append(make<Track>(m_time));
|
||||
}
|
||||
|
||||
void TrackManager::next_track()
|
||||
{
|
||||
if (++m_current_track >= m_tracks.size())
|
||||
m_current_track = 0;
|
||||
}
|
74
Userland/Applications/Piano/TrackManager.h
Normal file
74
Userland/Applications/Piano/TrackManager.h
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 "Track.h"
|
||||
#include <AK/Array.h>
|
||||
#include <AK/Noncopyable.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
class TrackManager {
|
||||
AK_MAKE_NONCOPYABLE(TrackManager);
|
||||
AK_MAKE_NONMOVABLE(TrackManager);
|
||||
|
||||
public:
|
||||
TrackManager();
|
||||
~TrackManager();
|
||||
|
||||
Track& current_track() { return *m_tracks[m_current_track]; }
|
||||
Span<const Sample> buffer() const { return m_current_front_buffer; }
|
||||
int octave() const { return m_octave; }
|
||||
int octave_base() const { return (m_octave - octave_min) * 12; }
|
||||
int time() const { return m_time; }
|
||||
|
||||
void fill_buffer(Span<Sample>);
|
||||
void reset();
|
||||
void set_should_loop(bool b) { m_should_loop = b; }
|
||||
void set_note_current_octave(int note, Switch);
|
||||
void set_octave(Direction);
|
||||
void set_octave(int octave);
|
||||
void add_track();
|
||||
void next_track();
|
||||
|
||||
private:
|
||||
Vector<NonnullOwnPtr<Track>> m_tracks;
|
||||
size_t m_current_track { 0 };
|
||||
|
||||
Array<Sample, sample_count> m_front_buffer;
|
||||
Array<Sample, sample_count> m_back_buffer;
|
||||
Span<Sample> m_current_front_buffer { m_front_buffer.span() };
|
||||
Span<Sample> m_current_back_buffer { m_back_buffer.span() };
|
||||
|
||||
int m_octave { 4 };
|
||||
|
||||
u32 m_time { 0 };
|
||||
|
||||
bool m_should_loop { true };
|
||||
};
|
88
Userland/Applications/Piano/WaveWidget.cpp
Normal file
88
Userland/Applications/Piano/WaveWidget.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 "TrackManager.h"
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
|
||||
WaveWidget::WaveWidget(TrackManager& track_manager)
|
||||
: m_track_manager(track_manager)
|
||||
{
|
||||
}
|
||||
|
||||
WaveWidget::~WaveWidget()
|
||||
{
|
||||
}
|
||||
|
||||
int WaveWidget::sample_to_y(int sample) const
|
||||
{
|
||||
constexpr int nice_scale_factor = 4;
|
||||
sample *= nice_scale_factor;
|
||||
constexpr double sample_max = NumericLimits<i16>::max();
|
||||
double percentage = sample / sample_max;
|
||||
double portion_of_half_height = percentage * ((frame_inner_rect().height() - 1) / 2.0);
|
||||
double y = (frame_inner_rect().height() / 2.0) + portion_of_half_height;
|
||||
return y;
|
||||
}
|
||||
|
||||
void WaveWidget::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Painter painter(*this);
|
||||
painter.fill_rect(frame_inner_rect(), Color::Black);
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
|
||||
Color left_wave_color = left_wave_colors[m_track_manager.current_track().wave()];
|
||||
Color right_wave_color = right_wave_colors[m_track_manager.current_track().wave()];
|
||||
auto buffer = m_track_manager.buffer();
|
||||
double width_scale = static_cast<double>(frame_inner_rect().width()) / buffer.size();
|
||||
|
||||
int prev_x = 0;
|
||||
int prev_y_left = sample_to_y(buffer[0].left);
|
||||
int prev_y_right = sample_to_y(buffer[0].right);
|
||||
painter.set_pixel({ prev_x, prev_y_left }, left_wave_color);
|
||||
painter.set_pixel({ prev_x, prev_y_right }, right_wave_color);
|
||||
|
||||
for (size_t x = 1; x < buffer.size(); ++x) {
|
||||
int y_left = sample_to_y(buffer[x].left);
|
||||
int y_right = sample_to_y(buffer[x].right);
|
||||
|
||||
Gfx::IntPoint point1_left(prev_x * width_scale, prev_y_left);
|
||||
Gfx::IntPoint point2_left(x * width_scale, y_left);
|
||||
painter.draw_line(point1_left, point2_left, left_wave_color);
|
||||
|
||||
Gfx::IntPoint point1_right(prev_x * width_scale, prev_y_right);
|
||||
Gfx::IntPoint point2_right(x * width_scale, y_right);
|
||||
painter.draw_line(point1_right, point2_right, right_wave_color);
|
||||
|
||||
prev_x = x;
|
||||
prev_y_left = y_left;
|
||||
prev_y_right = y_right;
|
||||
}
|
||||
|
||||
GUI::Frame::paint_event(event);
|
||||
}
|
47
Userland/Applications/Piano/WaveWidget.h
Normal file
47
Userland/Applications/Piano/WaveWidget.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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/Frame.h>
|
||||
|
||||
class TrackManager;
|
||||
|
||||
class WaveWidget final : public GUI::Frame {
|
||||
C_OBJECT(WaveWidget)
|
||||
public:
|
||||
virtual ~WaveWidget() override;
|
||||
|
||||
private:
|
||||
explicit WaveWidget(TrackManager&);
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
|
||||
int sample_to_y(int sample) const;
|
||||
|
||||
TrackManager& m_track_manager;
|
||||
};
|
137
Userland/Applications/Piano/main.cpp
Normal file
137
Userland/Applications/Piano/main.cpp
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 "TrackManager.h"
|
||||
#include <AK/Array.h>
|
||||
#include <LibAudio/ClientConnection.h>
|
||||
#include <LibAudio/WavWriter.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/FilePicker.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/MenuBar.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/Window.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibThread/Thread.h>
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix fattr", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto app = GUI::Application::construct(argc, argv);
|
||||
|
||||
if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto audio_client = Audio::ClientConnection::construct();
|
||||
audio_client->handshake();
|
||||
|
||||
TrackManager track_manager;
|
||||
|
||||
auto app_icon = GUI::Icon::default_icon("app-piano");
|
||||
auto window = GUI::Window::construct();
|
||||
auto& main_widget = window->set_main_widget<MainWidget>(track_manager);
|
||||
window->set_title("Piano");
|
||||
window->resize(840, 600);
|
||||
window->set_icon(app_icon.bitmap_for_size(16));
|
||||
window->show();
|
||||
|
||||
Audio::WavWriter wav_writer;
|
||||
Optional<String> save_path;
|
||||
bool need_to_write_wav = false;
|
||||
|
||||
auto audio_thread = LibThread::Thread::construct([&] {
|
||||
auto audio = Core::File::construct("/dev/audio");
|
||||
if (!audio->open(Core::IODevice::WriteOnly)) {
|
||||
dbgln("Can't open audio device: {}", audio->error_string());
|
||||
return 1;
|
||||
}
|
||||
|
||||
Array<Sample, sample_count> buffer;
|
||||
while (!Core::EventLoop::current().was_exit_requested()) {
|
||||
track_manager.fill_buffer(buffer);
|
||||
audio->write(reinterpret_cast<u8*>(buffer.data()), buffer_size);
|
||||
Core::EventLoop::current().post_event(main_widget, make<Core::CustomEvent>(0));
|
||||
Core::EventLoop::wake();
|
||||
|
||||
if (need_to_write_wav) {
|
||||
need_to_write_wav = false;
|
||||
track_manager.reset();
|
||||
track_manager.set_should_loop(false);
|
||||
do {
|
||||
track_manager.fill_buffer(buffer);
|
||||
wav_writer.write_samples(reinterpret_cast<u8*>(buffer.data()), buffer_size);
|
||||
} while (track_manager.time());
|
||||
track_manager.reset();
|
||||
track_manager.set_should_loop(true);
|
||||
wav_writer.finalize();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
audio_thread->start();
|
||||
|
||||
auto menubar = GUI::MenuBar::construct();
|
||||
|
||||
auto& app_menu = menubar->add_menu("Piano");
|
||||
app_menu.add_action(GUI::Action::create("Export", { Mod_Ctrl, Key_E }, [&](const GUI::Action&) {
|
||||
save_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "wav");
|
||||
if (!save_path.has_value())
|
||||
return;
|
||||
wav_writer.set_file(save_path.value());
|
||||
if (wav_writer.has_error()) {
|
||||
GUI::MessageBox::show(window, String::formatted("Failed to export WAV file: {}", wav_writer.error_string()), "Error", GUI::MessageBox::Type::Error);
|
||||
wav_writer.clear_error();
|
||||
return;
|
||||
}
|
||||
need_to_write_wav = true;
|
||||
}));
|
||||
app_menu.add_separator();
|
||||
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
|
||||
GUI::Application::the()->quit();
|
||||
return;
|
||||
}));
|
||||
|
||||
auto& edit_menu = menubar->add_menu("Edit");
|
||||
main_widget.add_actions(edit_menu);
|
||||
|
||||
auto& help_menu = menubar->add_menu("Help");
|
||||
help_menu.add_action(GUI::CommonActions::make_about_action("Piano", app_icon, window));
|
||||
|
||||
app->set_menubar(move(menubar));
|
||||
|
||||
return app->exec();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue