diff --git a/Applications/Piano/AudioEngine.cpp b/Applications/Piano/AudioEngine.cpp index 50a7f57ce8..39da5d9930 100644 --- a/Applications/Piano/AudioEngine.cpp +++ b/Applications/Piano/AudioEngine.cpp @@ -46,11 +46,19 @@ void AudioEngine::fill_buffer(FixedArray& buffer) { memset(buffer.data(), 0, buffer_size); - if (m_time == 0) - set_notes_from_roll(); - for (size_t i = 0; i < buffer.size(); ++i) { 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; @@ -104,25 +112,21 @@ void AudioEngine::fill_buffer(FixedArray& buffer) buffer[i].left += sample.left * m_power[note] * volume; buffer[i].right += sample.right * m_power[note] * volume; } - } - if (m_delay) { - if (m_delay_buffers.size() >= static_cast(m_delay)) { - auto to_blend = m_delay_buffers.dequeue(); - for (size_t i = 0; i < to_blend->size(); ++i) { - buffer[i].left += (*to_blend)[i].left * 0.333333; - buffer[i].right += (*to_blend)[i].right * 0.333333; - } + if (m_delay) { + buffer[i].left += m_delay_buffer[m_delay_index].left * 0.333333; + buffer[i].right += m_delay_buffer[m_delay_index].right * 0.333333; + m_delay_buffer[m_delay_index].left = buffer[i].left; + m_delay_buffer[m_delay_index].right = buffer[i].right; + if (++m_delay_index >= m_delay_samples) + m_delay_index = 0; } - auto delay_buffer = make>(buffer.size()); - memcpy(delay_buffer->data(), buffer.data(), buffer_size); - m_delay_buffers.enqueue(move(delay_buffer)); - } - - if (++m_time == m_tick) { - m_time = 0; - update_roll(); + if (++m_time >= roll_length) { + m_time = 0; + if (!m_should_loop) + break; + } } memcpy(m_back_buffer_ptr->data(), buffer.data(), buffer_size); @@ -136,15 +140,14 @@ void AudioEngine::reset() m_front_buffer_ptr = &m_front_buffer; m_back_buffer_ptr = &m_back_buffer; - m_delay_buffers.clear(); + 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)); m_time = 0; - m_current_column = 0; - m_previous_column = horizontal_notes - 1; } String AudioEngine::set_recorded_sample(const StringView& path) @@ -280,33 +283,54 @@ void AudioEngine::set_note_current_octave(int note, Switch switch_note) set_note(note + octave_base(), switch_note); } -void AudioEngine::set_roll_note(int y, int x, Switch switch_note) +void AudioEngine::sync_roll(int note) { - ASSERT(x >= 0 && x < horizontal_notes); - ASSERT(y >= 0 && y < note_count); - - m_roll_notes[y][x] = switch_note; - - if (x == m_current_column && switch_note == Off) // If you turn off a note that is playing. - set_note((note_count - 1) - y, Off); + auto it = m_roll_notes[note].find([&](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 AudioEngine::update_roll() +void AudioEngine::set_roll_note(int note, u32 on_sample, u32 off_sample) { - if (++m_current_column == horizontal_notes) - m_current_column = 0; - if (++m_previous_column == horizontal_notes) - m_previous_column = 0; -} + RollNote new_roll_note = { on_sample, off_sample }; -void AudioEngine::set_notes_from_roll() -{ - for (int note = 0; note < note_count; ++note) { - if (m_roll_notes[note][m_previous_column] == On) - set_note((note_count - 1) - note, Off); - if (m_roll_notes[note][m_current_column] == On) - set_note((note_count - 1) - note, On); + 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.off_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; + } + if (it->on_sample < new_roll_note.on_sample && it->off_sample >= new_roll_note.on_sample) { + if (m_time >= new_roll_note.off_sample && m_time <= it->off_sample) + set_note(note, Off); + it->off_sample = new_roll_note.on_sample - 1; + ASSERT(it->length() >= 2); + } + ++it; } + + m_roll_notes[note].append(new_roll_note); + sync_roll(note); } void AudioEngine::set_octave(Direction direction) @@ -373,6 +397,9 @@ void AudioEngine::set_release(int release) void AudioEngine::set_delay(int delay) { ASSERT(delay >= 0); - m_delay_buffers.clear(); 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; } diff --git a/Applications/Piano/AudioEngine.h b/Applications/Piano/AudioEngine.h index 922278a3fa..aae9554520 100644 --- a/Applications/Piano/AudioEngine.h +++ b/Applications/Piano/AudioEngine.h @@ -30,9 +30,11 @@ #include "Music.h" #include #include -#include +#include #include +typedef AK::SinglyLinkedListIterator, RollNote> RollIter; + class AudioEngine { AK_MAKE_NONCOPYABLE(AudioEngine) AK_MAKE_NONMOVABLE(AudioEngine) @@ -42,8 +44,7 @@ public: const FixedArray& buffer() const { return *m_front_buffer_ptr; } const Vector& recorded_sample() const { return m_recorded_sample; } - Switch roll_note(int y, int x) const { return m_roll_notes[y][x]; } - int current_column() const { return m_current_column; } + const SinglyLinkedList& roll_notes(int note) const { return m_roll_notes[note]; } int octave() const { return m_octave; } int octave_base() const { return (m_octave - octave_min) * 12; } int wave() const { return m_wave; } @@ -53,14 +54,14 @@ public: int release() const { return m_release; } int delay() const { return m_delay; } int time() const { return m_time; } - int tick() const { return m_tick; } void fill_buffer(FixedArray& buffer); void reset(); + void set_should_loop(bool b) { m_should_loop = b; } String set_recorded_sample(const StringView& path); void set_note(int note, Switch); void set_note_current_octave(int note, Switch); - void set_roll_note(int y, int x, Switch); + void set_roll_note(int note, u32 on_sample, u32 off_sample); void set_octave(Direction); void set_wave(int wave); void set_wave(Direction); @@ -78,9 +79,7 @@ private: Audio::Sample noise() const; Audio::Sample recorded_sample(size_t note); - void update_roll(); - void set_notes_from_roll(); - + void sync_roll(int note); void set_sustain_impl(int sustain); FixedArray m_front_buffer { sample_count }; @@ -88,7 +87,7 @@ private: FixedArray* m_front_buffer_ptr { &m_front_buffer }; FixedArray* m_back_buffer_ptr { &m_back_buffer }; - Queue>> m_delay_buffers; + Vector m_delay_buffer; Vector m_recorded_sample; @@ -108,11 +107,13 @@ private: 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 }; - int m_time { 0 }; - int m_tick { 8 }; + u32 m_time { 0 }; - Switch m_roll_notes[note_count][horizontal_notes] { { Off } }; - int m_current_column { 0 }; - int m_previous_column { horizontal_notes - 1 }; + bool m_should_loop { true }; + + SinglyLinkedList m_roll_notes[note_count]; + RollIter m_roll_iters[note_count]; }; diff --git a/Applications/Piano/KnobsWidget.cpp b/Applications/Piano/KnobsWidget.cpp index 0a4bf4b02f..5fc10c4fb3 100644 --- a/Applications/Piano/KnobsWidget.cpp +++ b/Applications/Piano/KnobsWidget.cpp @@ -63,7 +63,7 @@ KnobsWidget::KnobsWidget(AudioEngine& audio_engine, MainWidget& main_widget) m_decay_value = m_values_container->add(String::number(m_audio_engine.decay())); m_sustain_value = m_values_container->add(String::number(m_audio_engine.sustain())); m_release_value = m_values_container->add(String::number(m_audio_engine.release())); - m_delay_value = m_values_container->add(String::number(m_audio_engine.delay() / m_audio_engine.tick())); + m_delay_value = m_values_container->add(String::number(m_audio_engine.delay())); m_knobs_container = add(); m_knobs_container->set_layout(make()); @@ -144,12 +144,12 @@ KnobsWidget::KnobsWidget(AudioEngine& audio_engine, MainWidget& main_widget) constexpr int max_delay = 8; m_delay_knob = m_knobs_container->add(); m_delay_knob->set_range(0, max_delay); - m_delay_knob->set_value(max_delay - (m_audio_engine.delay() / m_audio_engine.tick())); + m_delay_knob->set_value(max_delay - m_audio_engine.delay()); m_delay_knob->on_value_changed = [this](int value) { - int new_delay = m_audio_engine.tick() * (max_delay - value); + int new_delay = max_delay - value; m_audio_engine.set_delay(new_delay); ASSERT(new_delay == m_audio_engine.delay()); - m_delay_value->set_text(String::number(new_delay / m_audio_engine.tick())); + m_delay_value->set_text(String::number(new_delay)); }; } diff --git a/Applications/Piano/MainWidget.cpp b/Applications/Piano/MainWidget.cpp index d8eab8d8ad..6010a51fe4 100644 --- a/Applications/Piano/MainWidget.cpp +++ b/Applications/Piano/MainWidget.cpp @@ -79,9 +79,7 @@ MainWidget::~MainWidget() void MainWidget::custom_event(Core::CustomEvent&) { m_wave_widget->update(); - - if (m_audio_engine.time() == 0) - m_roll_widget->update(); + m_roll_widget->update(); } void MainWidget::keydown_event(GUI::KeyEvent& event) diff --git a/Applications/Piano/Music.h b/Applications/Piano/Music.h index c6a2c4fd7a..c2736c067e 100644 --- a/Applications/Piano/Music.h +++ b/Applications/Piano/Music.h @@ -56,6 +56,13 @@ enum Switch { On, }; +struct RollNote { + u32 length() const { return (off_sample - on_sample) + 1; } + + u32 on_sample; + u32 off_sample; +}; + enum Direction { Down, Up, @@ -196,7 +203,10 @@ constexpr int black_keys_per_octave = 5; constexpr int octave_min = 1; constexpr int octave_max = 7; -constexpr int horizontal_notes = 32; +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; // Equal temperament, A = 440Hz // We calculate note frequencies relative to A4: diff --git a/Applications/Piano/RollWidget.cpp b/Applications/Piano/RollWidget.cpp index 03b87413ac..b46989b0f1 100644 --- a/Applications/Piano/RollWidget.cpp +++ b/Applications/Piano/RollWidget.cpp @@ -29,9 +29,13 @@ #include "AudioEngine.h" #include #include +#include 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(AudioEngine& audio_engine) : m_audio_engine(audio_engine) @@ -47,10 +51,21 @@ RollWidget::~RollWidget() void RollWidget::paint_event(GUI::PaintEvent& event) { - int roll_width = widget_inner_rect().width(); - double note_width = static_cast(roll_width) / horizontal_notes; + m_roll_width = widget_inner_rect().width() * m_zoom_level; + set_content_size({ m_roll_width, roll_height }); - set_content_size({ 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(log2(m_num_notes / time_signature_notes))); + m_note_width = static_cast(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. @@ -63,25 +78,29 @@ void RollWidget::paint_event(GUI::PaintEvent& event) 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.translate(0, -note_offset_remainder); + painter.translate(-horizontal_note_offset_remainder, -note_offset_remainder); + painter.add_clip_rect(event.rect()); for (int y = 0; y < notes_to_paint; ++y) { int y_pos = y * note_height; - for (int x = 0; x < horizontal_notes; ++x) { + for (int x = 0; x < horizontal_notes_to_paint; ++x) { // This is needed to avoid rounding errors. You can't just use - // note_width as the width. - int x_pos = x * note_width; - int next_x_pos = (x + 1) * note_width; + // 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::Rect rect(x_pos, y_pos, distance_to_next_x, note_height); - if (m_audio_engine.roll_note(y + note_offset, x) == On) - painter.fill_rect(rect, note_pressed_color); - else if (x == m_audio_engine.current_column()) - painter.fill_rect(rect, column_playing_color); - else if (key_pattern[key_pattern_index] == Black) + if (key_pattern[key_pattern_index] == Black) painter.fill_rect(rect, Color::LightGray); else painter.fill_rect(rect, Color::White); @@ -94,6 +113,31 @@ void RollWidget::paint_event(GUI::PaintEvent& event) 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) { + for (auto roll_note : m_audio_engine.roll_notes(note)) { + int x = m_roll_width * (static_cast(roll_note.on_sample) / roll_length); + int width = m_roll_width * (static_cast(roll_note.length()) / roll_length); + if (x + width < x_offset || x > x_offset + widget_inner_rect().width()) + continue; + if (width < 2) + width = 2; + + int y = ((note_count - 1) - note) * note_height; + int height = note_height; + + Gfx::Rect rect(x, y, width, height); + painter.fill_rect(rect, note_pressed_color); + painter.draw_rect(rect, Color::Black); + } + } + + int x = m_roll_width * (static_cast(m_audio_engine.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); } @@ -102,25 +146,61 @@ void RollWidget::mousedown_event(GUI::MouseEvent& event) if (!widget_inner_rect().contains(event.x(), event.y())) return; - int roll_width = widget_inner_rect().width(); - double note_width = static_cast(roll_width) / horizontal_notes; - int y = (event.y() + vertical_scrollbar().value()) - frame_thickness(); y /= note_height; - // There's a case where we can't just use x / note_width. For example, if - // your note_width is 3.1 you will have a rect starting at 3. When that + // 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 note_width is + // 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 = event.x() - frame_thickness(); - bool note_width_is_fractional = note_width - static_cast(note_width) != 0; + int x = (event.x() + horizontal_scrollbar().value()) - frame_thickness(); + bool note_width_is_fractional = m_note_width - static_cast(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 /= note_width; + x /= m_note_width; - m_audio_engine.set_roll_note(y, x, m_audio_engine.roll_note(y, x) == On ? Off : On); + int note = (note_count - 1) - y; + u32 on_sample = roll_length * (static_cast(x) / m_num_notes); + u32 off_sample = (roll_length * (static_cast(x + 1) / m_num_notes)) - 1; + m_audio_engine.set_roll_note(note, on_sample, off_sample); update(); } + +// 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); +} diff --git a/Applications/Piano/RollWidget.h b/Applications/Piano/RollWidget.h index e83597931a..235d424412 100644 --- a/Applications/Piano/RollWidget.h +++ b/Applications/Piano/RollWidget.h @@ -42,6 +42,12 @@ private: virtual void paint_event(GUI::PaintEvent&) override; virtual void mousedown_event(GUI::MouseEvent& event) override; + virtual void mousewheel_event(GUI::MouseEvent&) override; AudioEngine& m_audio_engine; + + int m_roll_width; + int m_num_notes; + double m_note_width; + int m_zoom_level { 1 }; }; diff --git a/Applications/Piano/main.cpp b/Applications/Piano/main.cpp index 24e58a2522..13d1573422 100644 --- a/Applications/Piano/main.cpp +++ b/Applications/Piano/main.cpp @@ -80,11 +80,13 @@ int main(int argc, char** argv) if (need_to_write_wav) { need_to_write_wav = false; audio_engine.reset(); - while (audio_engine.current_column() < horizontal_notes - 1) { + audio_engine.set_should_loop(false); + do { audio_engine.fill_buffer(buffer); wav_writer.write_samples(reinterpret_cast(buffer.data()), buffer_size); - } + } while (audio_engine.time()); audio_engine.reset(); + audio_engine.set_should_loop(true); wav_writer.finalize(); } }