1
Fork 0
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:
Andreas Kling 2021-01-12 12:05:23 +01:00
parent aa939c4b4b
commit dc28c07fa5
287 changed files with 1 additions and 1 deletions

View 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)

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

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

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

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

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

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

View 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;

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

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

View 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()
{
}

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

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

View 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];
};

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

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

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

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

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