diff --git a/Userland/Applications/Piano/AudioPlayerLoop.cpp b/Userland/Applications/Piano/AudioPlayerLoop.cpp index c6f36f5a4c..05e1401361 100644 --- a/Userland/Applications/Piano/AudioPlayerLoop.cpp +++ b/Userland/Applications/Piano/AudioPlayerLoop.cpp @@ -15,17 +15,9 @@ #include #include -static FixedArray music_samples_to_buffer(Vector& music_samples) -{ - FixedArray samples = MUST(FixedArray::try_create(music_samples.size())); - for (size_t i = 0; i < music_samples.size(); ++i) - samples[i] = { static_cast(music_samples[i].left) / AK::NumericLimits::max(), static_cast(music_samples[i].right) / AK::NumericLimits::max() }; - - return samples; -} - AudioPlayerLoop::AudioPlayerLoop(TrackManager& track_manager, bool& need_to_write_wav, Audio::WavWriter& wav_writer) : m_track_manager(track_manager) + , m_buffer(FixedArray::must_create_but_fixme_should_propagate_errors(sample_count)) , m_need_to_write_wav(need_to_write_wav) , m_wav_writer(wav_writer) { @@ -34,7 +26,7 @@ AudioPlayerLoop::AudioPlayerLoop(TrackManager& track_manager, bool& need_to_writ auto target_sample_rate = m_audio_client->get_sample_rate(); if (target_sample_rate == 0) target_sample_rate = Music::sample_rate; - m_resampler = Audio::ResampleHelper(Music::sample_rate, target_sample_rate); + m_resampler = Audio::ResampleHelper(Music::sample_rate, target_sample_rate); // FIXME: I said I would never write such a hack again, but here we are. // This code should die as soon as possible anyways, so it doesn't matter. @@ -44,7 +36,7 @@ AudioPlayerLoop::AudioPlayerLoop(TrackManager& track_manager, bool& need_to_writ void AudioPlayerLoop::timer_event(Core::TimerEvent&) { - if (m_audio_client->remaining_samples() < buffer_size) + if (m_audio_client->remaining_samples() < sample_count) enqueue_audio(); } @@ -53,8 +45,7 @@ void AudioPlayerLoop::enqueue_audio() m_track_manager.fill_buffer(m_buffer); // FIXME: Handle OOM better. auto audio_buffer = m_resampler->resample(m_buffer); - auto real_buffer = music_samples_to_buffer(audio_buffer); - (void)m_audio_client->async_enqueue(real_buffer); + (void)m_audio_client->async_enqueue(audio_buffer); // FIXME: This should be done somewhere else. if (m_need_to_write_wav) { @@ -63,7 +54,7 @@ void AudioPlayerLoop::enqueue_audio() m_track_manager.set_should_loop(false); do { m_track_manager.fill_buffer(m_buffer); - m_wav_writer.write_samples(reinterpret_cast(m_buffer.data()), buffer_size); + m_wav_writer.write_samples(m_buffer.span()); } while (m_track_manager.transport()->time()); m_track_manager.reset(); m_track_manager.set_should_loop(true); diff --git a/Userland/Applications/Piano/AudioPlayerLoop.h b/Userland/Applications/Piano/AudioPlayerLoop.h index 90e3d0d9e8..6dace31bd1 100644 --- a/Userland/Applications/Piano/AudioPlayerLoop.h +++ b/Userland/Applications/Piano/AudioPlayerLoop.h @@ -14,6 +14,7 @@ #include #include #include +#include class TrackManager; @@ -33,8 +34,8 @@ private: virtual void timer_event(Core::TimerEvent&) override; TrackManager& m_track_manager; - Array m_buffer; - Optional> m_resampler; + FixedArray m_buffer; + Optional> m_resampler; RefPtr m_audio_client; bool m_should_play_audio = true; diff --git a/Userland/Applications/Piano/CMakeLists.txt b/Userland/Applications/Piano/CMakeLists.txt index 60a49f7731..377b892a49 100644 --- a/Userland/Applications/Piano/CMakeLists.txt +++ b/Userland/Applications/Piano/CMakeLists.txt @@ -7,7 +7,6 @@ serenity_component( set(SOURCES AudioPlayerLoop.cpp - Track.cpp TrackManager.cpp KeysWidget.cpp KnobsWidget.cpp diff --git a/Userland/Applications/Piano/KnobsWidget.cpp b/Userland/Applications/Piano/KnobsWidget.cpp index 6a5a00d83f..4c9c7538cb 100644 --- a/Userland/Applications/Piano/KnobsWidget.cpp +++ b/Userland/Applications/Piano/KnobsWidget.cpp @@ -34,7 +34,7 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) m_values_container->set_layout(); m_values_container->set_fixed_height(10); - m_volume_value = m_values_container->add(String::number(m_track_manager.current_track().volume())); + m_volume_value = m_values_container->add(String::number(m_track_manager.current_track()->volume())); m_octave_value = m_values_container->add(String::number(m_track_manager.keyboard()->virtual_keyboard_octave())); m_knobs_container = add(); @@ -44,13 +44,13 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) m_volume_knob = m_knobs_container->add(); m_volume_knob->set_range(0, volume_max); - m_volume_knob->set_value(volume_max - m_track_manager.current_track().volume()); + m_volume_knob->set_value(volume_max - m_track_manager.current_track()->volume()); m_volume_knob->set_step(10); m_volume_knob->on_change = [this](int value) { int new_volume = volume_max - value; if (m_change_underlying) - m_track_manager.current_track().set_volume(new_volume); - VERIFY(new_volume == m_track_manager.current_track().volume()); + m_track_manager.current_track()->set_volume(new_volume); + VERIFY(new_volume == m_track_manager.current_track()->volume()); m_volume_value->set_text(String::number(new_volume)); }; @@ -67,7 +67,7 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) m_octave_value->set_text(String::number(new_octave)); }; - for (auto& raw_parameter : m_track_manager.current_track().synth()->parameters()) { + for (auto& raw_parameter : m_track_manager.current_track()->synth()->parameters()) { // The synth has range and enum parameters switch (raw_parameter.type()) { case DSP::ParameterType::Range: { @@ -94,7 +94,7 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) } } - for (auto& raw_parameter : m_track_manager.current_track().delay()->parameters()) { + for (auto& raw_parameter : m_track_manager.current_track()->delay()->parameters()) { // FIXME: We shouldn't do that, but we know the effect and it's nice. auto& parameter = static_cast(raw_parameter); m_delay_values.append(m_values_container->add(String::number(static_cast(parameter.value())))); @@ -116,7 +116,7 @@ void KnobsWidget::update_knobs() // need to change the slider without changing the underlying value. m_change_underlying = false; - m_volume_knob->set_value(volume_max - m_track_manager.current_track().volume()); + m_volume_knob->set_value(volume_max - m_track_manager.current_track()->volume()); m_octave_knob->set_value(octave_max - m_track_manager.keyboard()->virtual_keyboard_octave()); m_change_underlying = true; diff --git a/Userland/Applications/Piano/Music.h b/Userland/Applications/Piano/Music.h index e1f632ab9d..8f2b2a3a2e 100644 --- a/Userland/Applications/Piano/Music.h +++ b/Userland/Applications/Piano/Music.h @@ -26,8 +26,6 @@ struct Sample { // HACK: needs to increase with device sample rate, but all of the sample_count stuff is static for now constexpr int sample_count = 1 << 10; -constexpr int buffer_size = sample_count * sizeof(Sample); - constexpr double sample_rate = 44100; // Headroom for the synth diff --git a/Userland/Applications/Piano/ProcessorParameterWidget/Dropdown.h b/Userland/Applications/Piano/ProcessorParameterWidget/Dropdown.h index 326bf2b189..a600a97805 100644 --- a/Userland/Applications/Piano/ProcessorParameterWidget/Dropdown.h +++ b/Userland/Applications/Piano/ProcessorParameterWidget/Dropdown.h @@ -35,9 +35,9 @@ public: auto value = static_cast(model_index.row()); m_parameter.set_value_sneaky(value, DSP::Detail::ProcessorParameterSetValueTag {}); }; - m_parameter.did_change_value = [this](auto new_value) { + m_parameter.register_change_listener([this](auto new_value) { set_selected_index(static_cast(new_value)); - }; + }); } // Release focus when escape is pressed diff --git a/Userland/Applications/Piano/ProcessorParameterWidget/Slider.cpp b/Userland/Applications/Piano/ProcessorParameterWidget/Slider.cpp index 454484874d..515838f8d9 100644 --- a/Userland/Applications/Piano/ProcessorParameterWidget/Slider.cpp +++ b/Userland/Applications/Piano/ProcessorParameterWidget/Slider.cpp @@ -30,13 +30,16 @@ ProcessorParameterSlider::ProcessorParameterSlider(Orientation orientation, DSP: m_value_label->set_text(String::formatted("{:.2f}", static_cast(m_parameter))); on_change = [this](auto value) { + if (m_currently_setting_from_ui) + return; + m_currently_setting_from_ui = true; DSP::ParameterFixedPoint real_value; real_value.raw() = value; if (is_logarithmic()) // FIXME: Implement exponential for fixed point - real_value = exp(static_cast(real_value)); + real_value = exp2(static_cast(real_value)); - m_parameter.set_value_sneaky(real_value, DSP::Detail::ProcessorParameterSetValueTag {}); + m_parameter.set_value(real_value); if (m_value_label) { double value = static_cast(m_parameter); String label_text = String::formatted("{:.2f}", value); @@ -47,11 +50,12 @@ ProcessorParameterSlider::ProcessorParameterSlider(Orientation orientation, DSP: else m_value_label->set_text(label_text); } + m_currently_setting_from_ui = false; }; - m_parameter.did_change_value = [this](auto value) { + m_parameter.register_change_listener([this](auto value) { if (!is_logarithmic()) set_value(value.raw()); else set_value(value.log2().raw()); - }; + }); } diff --git a/Userland/Applications/Piano/ProcessorParameterWidget/Slider.h b/Userland/Applications/Piano/ProcessorParameterWidget/Slider.h index ea93937b76..9d028af9e0 100644 --- a/Userland/Applications/Piano/ProcessorParameterWidget/Slider.h +++ b/Userland/Applications/Piano/ProcessorParameterWidget/Slider.h @@ -29,4 +29,6 @@ protected: private: // Converts based on processor parameter boundaries. int linear_to_logarithmic(int linear_value); + + bool m_currently_setting_from_ui { false }; }; diff --git a/Userland/Applications/Piano/RollWidget.cpp b/Userland/Applications/Piano/RollWidget.cpp index 3756acaf4a..f358c15df8 100644 --- a/Userland/Applications/Piano/RollWidget.cpp +++ b/Userland/Applications/Piano/RollWidget.cpp @@ -8,6 +8,7 @@ */ #include "RollWidget.h" +#include "LibGUI/Event.h" #include "TrackManager.h" #include #include @@ -134,9 +135,9 @@ void RollWidget::paint_event(GUI::PaintEvent& event) 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)) { + for (auto const& clip : m_track_manager.current_track()->notes()) { + for (auto const& roll_note : clip->notes()) { + int y = ((note_count - 1) - roll_note.pitch) * note_height; 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()) @@ -150,7 +151,10 @@ void RollWidget::paint_event(GUI::PaintEvent& event) painter.fill_rect(rect, note_pressed_color); painter.draw_rect(rect, Color::Black); } + } + 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; Gfx::IntRect note_name_rect(3, y, 1, note_height); auto note_name = note_names[note % notes_per_octave]; @@ -197,7 +201,13 @@ void RollWidget::mousemove_event(GUI::MouseEvent& event) 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); + m_track_manager.current_track()->remove_note(m_note_drag_location.value()); + } + + // Right-Click deletes notes + if (event.button() == GUI::MouseButton::Secondary) { + update(); + return; } auto get_note_x = [&](int x0) { @@ -220,8 +230,8 @@ void RollWidget::mousemove_event(GUI::MouseEvent& event) u32 on_sample = roll_length * (static_cast(min(x0, x1)) / m_num_notes); u32 off_sample = (roll_length * (static_cast(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, (u8)m_drag_note, 0 }; + m_note_drag_location = RollNote { on_sample, off_sample, (u8)m_drag_note, 127 }; + m_track_manager.current_track()->set_note(m_note_drag_location.value()); update(); } diff --git a/Userland/Applications/Piano/SamplerWidget.cpp b/Userland/Applications/Piano/SamplerWidget.cpp index 372558cd53..2fcd6cfc24 100644 --- a/Userland/Applications/Piano/SamplerWidget.cpp +++ b/Userland/Applications/Piano/SamplerWidget.cpp @@ -33,36 +33,8 @@ void WaveEditor::paint_event(GUI::PaintEvent& 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(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; - } + // FIXME: This widget currently can't display anything. + return; } SamplerWidget::SamplerWidget(TrackManager& track_manager) diff --git a/Userland/Applications/Piano/Track.cpp b/Userland/Applications/Piano/Track.cpp deleted file mode 100644 index 892cb409de..0000000000 --- a/Userland/Applications/Piano/Track.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2018-2020, Andreas Kling - * Copyright (c) 2019-2020, William McPherson - * Copyright (c) 2021, kleines Filmröllchen - * Copyright (c) 2022, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "Track.h" -#include -#include -#include -#include -#include -#include -#include - -Track::Track(NonnullRefPtr transport, NonnullRefPtr keyboard) - : m_transport(move(transport)) - , m_delay(make_ref_counted(m_transport)) - , m_synth(make_ref_counted(m_transport)) - , m_keyboard(move(keyboard)) -{ - set_volume(volume_max); -} - -void Track::fill_sample(Sample& sample) -{ - auto playing_notes = DSP::RollNotes {}; - - for (size_t i = 0; i < note_count; ++i) { - bool has_roll_notes = false; - auto& notes_at_pitch = m_roll_notes[i]; - for (auto& note : notes_at_pitch) { - if (note.is_playing(m_transport->time())) { - has_roll_notes = true; - playing_notes.set(i, note); - } - } - if (m_is_active_track) { - auto key_at_pitch = m_keyboard->note_at(i); - if (key_at_pitch.has_value() && key_at_pitch.value().is_playing(m_transport->time())) - playing_notes.set(i, key_at_pitch.release_value()); - // If there are roll notes playing, don't stop them when we lift a keyboard key. - else if (!has_roll_notes) - playing_notes.remove(i); - } - } - - auto synthesized_sample = DSP::Signal { FixedArray::must_create_but_fixme_should_propagate_errors(1) }; - m_synth->process(playing_notes, synthesized_sample); - auto delayed_signal = DSP::Signal { FixedArray::must_create_but_fixme_should_propagate_errors(1) }; - m_delay->process(synthesized_sample, delayed_signal); - auto delayed_sample = delayed_signal.get>()[0]; - - // HACK: Convert to old Piano range: 16-bit int - delayed_sample *= NumericLimits::max(); - delayed_sample.left = clamp(delayed_sample.left, NumericLimits::min(), NumericLimits::max()); - delayed_sample.right = clamp(delayed_sample.right, NumericLimits::min(), NumericLimits::max()); - // TODO: Use the master processor - delayed_sample *= static_cast(m_volume) / static_cast(volume_max) * volume_factor; - - sample.left += delayed_sample.left; - sample.right += delayed_sample.right; -} - -void Track::reset() -{ - for (size_t note = 0; note < note_count; ++note) - m_roll_iterators[note] = m_roll_notes[note].begin(); -} - -void Track::sync_roll(int note) -{ - auto it = m_roll_notes[note].find_if([&](auto& roll_note) { return roll_note.off_sample > m_transport->time(); }); - if (it.is_end()) - m_roll_iterators[note] = m_roll_notes[note].begin(); - else - m_roll_iterators[note] = it; -} - -void Track::set_roll_note(int note, u32 on_sample, u32 off_sample) -{ - RollNote new_roll_note = { on_sample, off_sample, (u8)note, 0 }; - - VERIFY(note >= 0 && note < note_count); - VERIFY(new_roll_note.off_sample < roll_length); - VERIFY(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) { - it.remove(m_roll_notes[note]); - 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) { - it.remove(m_roll_notes[note]); - it = m_roll_notes[note].begin(); - continue; - } - ++it; - } - - m_roll_notes[note].append(new_roll_note); - sync_roll(note); -} - -void Track::set_volume(int volume) -{ - VERIFY(volume >= 0); - m_volume = volume; -} diff --git a/Userland/Applications/Piano/Track.h b/Userland/Applications/Piano/Track.h deleted file mode 100644 index a4e792a18d..0000000000 --- a/Userland/Applications/Piano/Track.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2018-2020, Andreas Kling - * Copyright (c) 2019-2020, William McPherson - * Copyright (c) 2021, kleines Filmröllchen - * Copyright (c) 2022, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include "Music.h" -#include -#include -#include -#include -#include -#include -#include -#include - -using DSP::RollNote; -using RollIter = AK::SinglyLinkedListIterator, RollNote>; - -class Track { - AK_MAKE_NONCOPYABLE(Track); - AK_MAKE_NONMOVABLE(Track); - -public: - Track(NonnullRefPtr, NonnullRefPtr); - ~Track() = default; - - Vector const& recorded_sample() const { return m_recorded_sample; } - SinglyLinkedList const& roll_notes(int note) const { return m_roll_notes[note]; } - int volume() const { return m_volume; } - NonnullRefPtr synth() { return m_synth; } - NonnullRefPtr delay() { return m_delay; } - - void fill_sample(Sample& sample); - void reset(); - String set_recorded_sample(StringView path); - void set_roll_note(int note, u32 on_sample, u32 off_sample); - void set_volume(int volume); - void set_active(bool active) { m_is_active_track = active; } - -private: - Audio::Sample recorded_sample(size_t note); - - void sync_roll(int note); - - Vector m_recorded_sample; - - int m_volume; - - NonnullRefPtr m_transport; - NonnullRefPtr m_delay; - NonnullRefPtr m_synth; - - SinglyLinkedList m_roll_notes[note_count]; - RollIter m_roll_iterators[note_count]; - NonnullRefPtr m_keyboard; - bool m_is_active_track { false }; -}; diff --git a/Userland/Applications/Piano/TrackManager.cpp b/Userland/Applications/Piano/TrackManager.cpp index e447f857da..2b189afc65 100644 --- a/Userland/Applications/Piano/TrackManager.cpp +++ b/Userland/Applications/Piano/TrackManager.cpp @@ -8,15 +8,18 @@ */ #include "TrackManager.h" -#include "Applications/Piano/Music.h" +#include "Music.h" #include +#include +#include +#include TrackManager::TrackManager() : m_transport(make_ref_counted(120, 4)) , m_keyboard(make_ref_counted(m_transport)) + , m_temporary_track_buffer(FixedArray::must_create_but_fixme_should_propagate_errors(sample_count)) { add_track(); - m_tracks[m_current_track]->set_active(true); } void TrackManager::time_forward(int amount) @@ -30,47 +33,38 @@ void TrackManager::time_forward(int amount) } } -void TrackManager::fill_buffer(Span buffer) +void TrackManager::fill_buffer(FixedArray& buffer) { - memset(buffer.data(), 0, buffer_size); + VERIFY(buffer.size() == m_temporary_track_buffer.size()); + size_t sample_count = buffer.size(); + // No need to zero the temp buffer as the track overwrites it anyways. + buffer.fill_with({}); - for (size_t i = 0; i < buffer.size(); ++i) { - for (auto& track : m_tracks) - track->fill_sample(buffer[i]); - - m_transport->set_time(m_transport->time() + 1); - // FIXME: This should be handled automatically by Transport. - if (m_transport->time() >= roll_length) { - m_transport->set_time(0); - if (!m_should_loop) - break; - } + for (auto& track : m_tracks) { + track->current_signal(m_temporary_track_buffer); + for (size_t i = 0; i < sample_count; ++i) + buffer[i] += m_temporary_track_buffer[i]; } - memcpy(m_current_back_buffer.data(), buffer.data(), buffer_size); - swap(m_current_front_buffer, m_current_back_buffer); + m_transport->set_time(m_transport->time() + sample_count); + // FIXME: This should be handled automatically by Transport. It will also advance slightly past the loop point if we're unlucky. + if (m_transport->time() >= roll_length) + m_transport->set_time(0); } 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_transport->set_time(0); - - for (auto& track : m_tracks) { - track->reset(); - track->set_active(false); - } - m_tracks[m_current_track]->set_active(true); } void TrackManager::add_track() { - m_tracks.append(make(m_transport, m_keyboard)); + auto new_track = make_ref_counted(m_transport, m_keyboard); + MUST(new_track->resize_internal_buffers_to(m_temporary_track_buffer.size())); + new_track->add_processor(make_ref_counted(m_transport)); + new_track->add_processor(make_ref_counted(m_transport)); + new_track->add_clip(0, roll_length); + m_tracks.append(move(new_track)); } int TrackManager::next_track_index() const diff --git a/Userland/Applications/Piano/TrackManager.h b/Userland/Applications/Piano/TrackManager.h index d9407777c9..73677c9fe3 100644 --- a/Userland/Applications/Piano/TrackManager.h +++ b/Userland/Applications/Piano/TrackManager.h @@ -10,13 +10,14 @@ #pragma once #include "Music.h" -#include "Track.h" #include +#include #include #include #include #include #include +#include class TrackManager { AK_MAKE_NONCOPYABLE(TrackManager); @@ -26,16 +27,12 @@ public: TrackManager(); ~TrackManager() = default; - Track& current_track() { return *m_tracks[m_current_track]; } - Span buffer() const { return m_current_front_buffer; } + NonnullRefPtr current_track() { return *m_tracks[m_current_track]; } int track_count() { return m_tracks.size(); }; void set_current_track(size_t track_index) { VERIFY((int)track_index < track_count()); - auto old_track = m_current_track; m_current_track = track_index; - m_tracks[old_track]->set_active(false); - m_tracks[m_current_track]->set_active(true); } NonnullRefPtr transport() const { return m_transport; } @@ -43,7 +40,7 @@ public: // Legacy API, do not add new users. void time_forward(int amount); - void fill_buffer(Span); + void fill_buffer(FixedArray&); void reset(); void set_keyboard_note(int note, DSP::Keyboard::Switch note_switch); void set_should_loop(bool b) { m_should_loop = b; } @@ -51,15 +48,12 @@ public: int next_track_index() const; private: - Vector> m_tracks; + Vector> m_tracks; NonnullRefPtr m_transport; NonnullRefPtr m_keyboard; size_t m_current_track { 0 }; - Array m_front_buffer; - Array m_back_buffer; - Span m_current_front_buffer { m_front_buffer.span() }; - Span m_current_back_buffer { m_back_buffer.span() }; + FixedArray m_temporary_track_buffer; bool m_should_loop { true }; }; diff --git a/Userland/Applications/Piano/WaveWidget.cpp b/Userland/Applications/Piano/WaveWidget.cpp index 25f1df548f..1a9edf03d9 100644 --- a/Userland/Applications/Piano/WaveWidget.cpp +++ b/Userland/Applications/Piano/WaveWidget.cpp @@ -33,9 +33,10 @@ void WaveWidget::paint_event(GUI::PaintEvent& event) 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().synth()->wave()]; - Color right_wave_color = right_wave_colors[m_track_manager.current_track().synth()->wave()]; - auto buffer = m_track_manager.buffer(); + Color left_wave_color = left_wave_colors[m_track_manager.current_track()->synth()->wave()]; + Color right_wave_color = right_wave_colors[m_track_manager.current_track()->synth()->wave()]; + // FIXME: We can't get the last buffer from the track manager anymore + auto buffer = FixedArray::must_create_but_fixme_should_propagate_errors(sample_count); double width_scale = static_cast(frame_inner_rect().width()) / buffer.size(); int prev_x = 0; diff --git a/Userland/Libraries/LibAudio/WavWriter.cpp b/Userland/Libraries/LibAudio/WavWriter.cpp index 4ba2524797..a7230b3cdc 100644 --- a/Userland/Libraries/LibAudio/WavWriter.cpp +++ b/Userland/Libraries/LibAudio/WavWriter.cpp @@ -40,10 +40,18 @@ void WavWriter::set_file(StringView path) m_finalized = false; } -void WavWriter::write_samples(u8 const* samples, size_t size) +void WavWriter::write_samples(Span samples) { - m_data_sz += size; - m_file->write(samples, size); + m_data_sz += samples.size() * sizeof(Sample); + + for (auto const& sample : samples) { + // FIXME: This only really works for 16-bit samples. + u16 left = static_cast(sample.left * static_cast(1 << m_bits_per_sample)); + u16 right = static_cast(sample.right * static_cast(1 << m_bits_per_sample)); + // FIXME: This ignores endianness. + m_file->write(bit_cast(&left), sizeof(u16)); + m_file->write(bit_cast(&right), sizeof(u16)); + } } void WavWriter::finalize() diff --git a/Userland/Libraries/LibAudio/WavWriter.h b/Userland/Libraries/LibAudio/WavWriter.h index bfb06caf6f..87cb4b7bec 100644 --- a/Userland/Libraries/LibAudio/WavWriter.h +++ b/Userland/Libraries/LibAudio/WavWriter.h @@ -8,6 +8,7 @@ #include #include +#include #include namespace Audio { @@ -24,7 +25,7 @@ public: bool has_error() const { return !m_error_string.is_null(); } char const* error_string() const { return m_error_string.characters(); } - void write_samples(u8 const* samples, size_t size); + void write_samples(Span samples); void finalize(); // You can finalize manually or let the destructor do it. u32 sample_rate() const { return m_sample_rate; } diff --git a/Userland/Libraries/LibDSP/CMakeLists.txt b/Userland/Libraries/LibDSP/CMakeLists.txt index 34fa31957a..986bd5bb36 100644 --- a/Userland/Libraries/LibDSP/CMakeLists.txt +++ b/Userland/Libraries/LibDSP/CMakeLists.txt @@ -1,9 +1,9 @@ set(SOURCES Clip.cpp - Track.cpp Effects.cpp Synthesizers.cpp Keyboard.cpp + Track.cpp ) serenity_lib(LibDSP dsp) diff --git a/Userland/Libraries/LibDSP/Clip.cpp b/Userland/Libraries/LibDSP/Clip.cpp index 09f9a16176..1a3464b863 100644 --- a/Userland/Libraries/LibDSP/Clip.cpp +++ b/Userland/Libraries/LibDSP/Clip.cpp @@ -16,30 +16,18 @@ Sample AudioClip::sample_at(u32 time) void NoteClip::set_note(RollNote note) { - VERIFY(note.pitch >= 0 && note.pitch < note_frequencies.size()); - VERIFY(note.off_sample < m_length); - VERIFY(note.length() >= 2); + m_notes.remove_all_matching([&](auto const& other) { + return other.pitch == note.pitch && other.overlaps_with(note); + }); + m_notes.append(note); +} - auto& notes = m_notes[note.pitch]; - for (auto it = notes.begin(); !it.is_end();) { - auto iterated_note = *it; - if (iterated_note.on_sample > note.off_sample) { - notes.insert_before(it, note); - return; - } - if (iterated_note.on_sample <= note.on_sample && iterated_note.off_sample >= note.on_sample) { - notes.remove(it); - return; - } - if ((note.on_sample == 0 || iterated_note.on_sample >= note.on_sample - 1) && iterated_note.on_sample <= note.off_sample) { - notes.remove(it); - it = notes.begin(); - continue; - } - ++it; - } - - notes.append(note); +void NoteClip::remove_note(RollNote note) +{ + // FIXME: See header; this could be much faster with a better datastructure. + m_notes.remove_first_matching([note](auto const& element) { + return element.on_sample == note.on_sample && element.off_sample == note.off_sample && element.pitch == note.pitch; + }); } } diff --git a/Userland/Libraries/LibDSP/Clip.h b/Userland/Libraries/LibDSP/Clip.h index a3511ffbc9..0d517cd65c 100644 --- a/Userland/Libraries/LibDSP/Clip.h +++ b/Userland/Libraries/LibDSP/Clip.h @@ -16,6 +16,12 @@ namespace DSP { // A clip is a self-contained snippet of notes or audio that can freely move inside and in between tracks. class Clip : public RefCounted { public: + Clip(u32 start, u32 length) + : m_start(start) + , m_length(length) + { + } + virtual ~Clip() = default; u32 start() const { return m_start; } @@ -23,12 +29,6 @@ public: u32 end() const { return m_start + m_length; } protected: - Clip(u32 start, u32 length) - : m_start(start) - , m_length(length) - { - } - u32 m_start; u32 m_length; }; @@ -45,12 +45,24 @@ private: class NoteClip final : public Clip { public: - void set_note(RollNote note); + NoteClip(u32 start, u32 length) + : Clip(start, length) + { + } - Array, note_frequencies.size()> const& notes() const { return m_notes; } + void set_note(RollNote note); + // May do nothing; that's fine. + void remove_note(RollNote note); + + Span notes() const { return m_notes.span(); } + + RollNote operator[](size_t index) const { return m_notes[index]; } + RollNote operator[](size_t index) { return m_notes[index]; } + bool is_empty() const { return m_notes.is_empty(); } private: - Array, note_frequencies.size()> m_notes; + // FIXME: Better datastructures to think about here: B-Trees or good ol' RBTrees (not very cache friendly) + Vector m_notes; }; } diff --git a/Userland/Libraries/LibDSP/Effects.cpp b/Userland/Libraries/LibDSP/Effects.cpp index 4df33748fd..c7c0fe9e92 100644 --- a/Userland/Libraries/LibDSP/Effects.cpp +++ b/Userland/Libraries/LibDSP/Effects.cpp @@ -20,6 +20,11 @@ Delay::Delay(NonnullRefPtr transport) m_parameters.append(m_delay_decay); m_parameters.append(m_delay_time); m_parameters.append(m_dry_gain); + + m_delay_time.register_change_listener([this](auto const&) { + this->handle_delay_time_change(); + }); + handle_delay_time_change(); } void Delay::handle_delay_time_change() @@ -35,9 +40,6 @@ void Delay::handle_delay_time_change() void Delay::process_impl(Signal const& input_signal, Signal& output_signal) { - // FIXME: This is allocating and needs to happen on a different thread. - handle_delay_time_change(); - auto const& samples = input_signal.get>(); auto& output = output_signal.get>(); for (size_t i = 0; i < output.size(); ++i) { diff --git a/Userland/Libraries/LibDSP/Keyboard.cpp b/Userland/Libraries/LibDSP/Keyboard.cpp index 0952aced49..eaa53e0d5c 100644 --- a/Userland/Libraries/LibDSP/Keyboard.cpp +++ b/Userland/Libraries/LibDSP/Keyboard.cpp @@ -16,7 +16,7 @@ void Keyboard::set_keyboard_note(u8 pitch, Keyboard::Switch note_switch) VERIFY(pitch < note_frequencies.size()); if (note_switch == Switch::Off) { - m_pressed_notes.remove(pitch); + m_pressed_notes[pitch] = {}; return; } @@ -27,7 +27,7 @@ void Keyboard::set_keyboard_note(u8 pitch, Keyboard::Switch note_switch) .velocity = NumericLimits::max(), }; - m_pressed_notes.set(pitch, fake_note); + m_pressed_notes[pitch] = fake_note; } void Keyboard::set_keyboard_note_in_active_octave(i8 octave_offset, Switch note_switch) { @@ -57,7 +57,7 @@ ErrorOr Keyboard::set_virtual_keyboard_octave(u8 octave) bool Keyboard::is_pressed(u8 pitch) const { - return m_pressed_notes.get(pitch).has_value() && m_pressed_notes.get(pitch)->is_playing(m_transport->time()); + return m_pressed_notes[pitch].has_value() && m_pressed_notes[pitch]->is_playing(m_transport->time()); } bool Keyboard::is_pressed_in_active_octave(i8 octave_offset) const diff --git a/Userland/Libraries/LibDSP/Keyboard.h b/Userland/Libraries/LibDSP/Keyboard.h index 5bb6f67b39..a4f42ed265 100644 --- a/Userland/Libraries/LibDSP/Keyboard.h +++ b/Userland/Libraries/LibDSP/Keyboard.h @@ -41,7 +41,7 @@ public: void set_keyboard_note_in_active_octave(i8 octave_offset, Switch note_switch); RollNotes const& notes() const { return m_pressed_notes; } - Optional note_at(u8 pitch) const { return m_pressed_notes.get(pitch); } + Optional note_at(u8 pitch) const { return m_pressed_notes[pitch]; } bool is_pressed(u8 pitch) const; bool is_pressed_in_active_octave(i8 octave_offset) const; diff --git a/Userland/Libraries/LibDSP/Music.h b/Userland/Libraries/LibDSP/Music.h index 182dad8b12..adcff712a0 100644 --- a/Userland/Libraries/LibDSP/Music.h +++ b/Userland/Libraries/LibDSP/Music.h @@ -60,6 +60,24 @@ struct RollNote { } constexpr bool is_playing(u32 time) const { return on_sample <= time && time <= off_sample; } + constexpr bool is_playing_during(u32 start_time, u32 end_time) const + { + // There are three scenarios for a playing note. + return + // 1. The note ends within our time frame. + (this->off_sample >= start_time && this->off_sample < end_time) + // 2. The note starts within our time frame. + || (this->on_sample >= start_time && this->on_sample < end_time) + // 3. The note starts before our time frame and ends after it. + || (this->on_sample < start_time && this->off_sample >= end_time); + } + + constexpr bool overlaps_with(RollNote const& other) const + { + // Notes don't overlap if one is completely before or behind the other. + return !((this->on_sample < other.on_sample && this->off_sample < other.on_sample) + || (this->on_sample >= other.off_sample && this->off_sample >= other.off_sample)); + } }; enum class SignalType : u8 { @@ -78,26 +96,6 @@ public: } }; -using RollNotes = OrderedHashMap; - -struct Signal : public Variant, RollNotes> { - using Variant::Variant; - AK_MAKE_NONCOPYABLE(Signal); - -public: - Signal& operator=(Signal&&) = default; - Signal(Signal&&) = default; - - ALWAYS_INLINE SignalType type() const - { - if (has>()) - return SignalType::Sample; - if (has()) - return SignalType::Note; - return SignalType::Invalid; - } -}; - // Equal temperament, A = 440Hz // We calculate note frequencies relative to A4: // 440.0 * pow(pow(2.0, 1.0 / 12.0), N) @@ -196,7 +194,27 @@ constexpr Array note_frequencies = { 3951.0664100489994, }; +using RollNotes = Array, note_frequencies.size()>; + constexpr size_t const notes_per_octave = 12; constexpr double const middle_c = note_frequencies[36]; +struct Signal : public Variant, RollNotes> { + using Variant::Variant; + AK_MAKE_NONCOPYABLE(Signal); + +public: + Signal& operator=(Signal&&) = default; + Signal(Signal&&) = default; + + ALWAYS_INLINE SignalType type() const + { + if (has>()) + return SignalType::Sample; + if (has()) + return SignalType::Note; + return SignalType::Invalid; + } +}; + } diff --git a/Userland/Libraries/LibDSP/ProcessorParameter.h b/Userland/Libraries/LibDSP/ProcessorParameter.h index 8134d95253..2e60dca151 100644 --- a/Userland/Libraries/LibDSP/ProcessorParameter.h +++ b/Userland/Libraries/LibDSP/ProcessorParameter.h @@ -79,7 +79,7 @@ public: void set_value(ParameterT value) { set_value_sneaky(value, DSP::Detail::ProcessorParameterSetValueTag {}); - if (did_change_value) + for (auto const& did_change_value : m_change_value_listeners) did_change_value(value); } @@ -90,10 +90,15 @@ public: m_value = value; } - Function did_change_value; + // FIXME: Devise a good API for unregistering listeners. + void register_change_listener(Function listener) + { + m_change_value_listeners.append(move(listener)); + } protected: ParameterT m_value; + Vector> m_change_value_listeners; }; } diff --git a/Userland/Libraries/LibDSP/Synthesizers.cpp b/Userland/Libraries/LibDSP/Synthesizers.cpp index b2e36ed0a8..27465833d2 100644 --- a/Userland/Libraries/LibDSP/Synthesizers.cpp +++ b/Userland/Libraries/LibDSP/Synthesizers.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -39,42 +40,45 @@ void Classic::process_impl(Signal const& input_signal, [[maybe_unused]] Signal& // Do this for every time step and set the signal accordingly. for (size_t sample_index = 0; sample_index < output_samples.size(); ++sample_index) { Sample& out = output_samples[sample_index]; + out = {}; u32 sample_time = m_transport->time() + sample_index; - SinglyLinkedList playing_envelopes; + Array, note_frequencies.size()> playing_envelopes; // "Press" the necessary notes in the internal representation, // and "release" all of the others for (u8 i = 0; i < note_frequencies.size(); ++i) { - if (auto maybe_note = in.get(i); maybe_note.has_value()) - m_playing_notes.set(i, maybe_note.value()); + if (auto maybe_note = in[i]; maybe_note.has_value()) + m_playing_notes[i] = maybe_note; - if (m_playing_notes.contains(i)) { - Envelope note_envelope = m_playing_notes.get(i)->to_envelope(sample_time, m_attack * m_transport->ms_sample_rate(), m_decay * m_transport->ms_sample_rate(), m_release * m_transport->ms_sample_rate()); + if (m_playing_notes[i].has_value()) { + Envelope note_envelope = m_playing_notes[i]->to_envelope(sample_time, m_attack * m_transport->ms_sample_rate(), m_decay * m_transport->ms_sample_rate(), m_release * m_transport->ms_sample_rate()); // There are two conditions for removing notes: // 1. The envelope has expired, regardless of whether the note was still given to us in the input. if (!note_envelope.is_active()) { - m_playing_notes.remove(i); + m_playing_notes[i] = {}; continue; } // 2. The envelope has not expired, but the note was not given to us. // This means that the note abruptly stopped playing; i.e. the audio infrastructure didn't know the length of the notes initially. // That basically means we're dealing with a keyboard note. Chop its end time to end now. - if (!note_envelope.is_release() && !in.get(i).has_value()) { + if (!note_envelope.is_release() && !in[i].has_value()) { // dbgln("note {} not released, setting release phase, envelope={}", i, note_envelope.envelope); note_envelope.set_release(0); - auto real_note = *m_playing_notes.get(i); + auto real_note = *m_playing_notes[i]; real_note.off_sample = sample_time; - m_playing_notes.set(i, real_note); + m_playing_notes[i] = real_note; } - playing_envelopes.append(PitchedEnvelope { note_envelope, i }); + playing_envelopes[i] = PitchedEnvelope { note_envelope, i }; } } for (auto envelope : playing_envelopes) { - double volume = volume_from_envelope(envelope); - double wave = wave_position(envelope.note); + if (!envelope.has_value()) + continue; + double volume = volume_from_envelope(*envelope); + double wave = wave_position(sample_time, envelope->note); out += volume * wave; } } @@ -100,19 +104,19 @@ double Classic::volume_from_envelope(Envelope const& envelope) const VERIFY_NOT_REACHED(); } -double Classic::wave_position(u8 note) +double Classic::wave_position(u32 sample_time, u8 note) { switch (m_waveform) { case Sine: - return sin_position(note); + return sin_position(sample_time, note); case Triangle: - return triangle_position(note); + return triangle_position(sample_time, note); case Square: - return square_position(note); + return square_position(sample_time, note); case Saw: - return saw_position(note); + return saw_position(sample_time, note); case Noise: - return noise_position(note); + return noise_position(sample_time, note); } VERIFY_NOT_REACHED(); } @@ -122,43 +126,43 @@ double Classic::samples_per_cycle(u8 note) const return m_transport->sample_rate() / note_frequencies[note]; } -double Classic::sin_position(u8 note) const +double Classic::sin_position(u32 sample_time, u8 note) const { double spc = samples_per_cycle(note); - double cycle_pos = m_transport->time() / spc; + double cycle_pos = sample_time / spc; return AK::sin(cycle_pos * 2 * AK::Pi); } // Absolute value of the saw wave "flips" the negative portion into the positive, creating a ramp up and down. -double Classic::triangle_position(u8 note) const +double Classic::triangle_position(u32 sample_time, u8 note) const { - double saw = saw_position(note); + double saw = saw_position(sample_time, note); return AK::fabs(saw) * 2 - 1; } // The first half of the cycle period is 1, the other half -1. -double Classic::square_position(u8 note) const +double Classic::square_position(u32 sample_time, u8 note) const { double spc = samples_per_cycle(note); - double progress = AK::fmod(static_cast(m_transport->time()), spc) / spc; + double progress = AK::fmod(static_cast(sample_time), spc) / spc; return progress >= 0.5 ? -1 : 1; } // Modulus creates inverse saw, which we need to flip and scale. -double Classic::saw_position(u8 note) const +double Classic::saw_position(u32 sample_time, u8 note) const { double spc = samples_per_cycle(note); - double unscaled = spc - AK::fmod(static_cast(m_transport->time()), spc); + double unscaled = spc - AK::fmod(static_cast(sample_time), spc); return unscaled / (samples_per_cycle(note) / 2.) - 1; } // We resample the noise twenty times per cycle. -double Classic::noise_position(u8 note) +double Classic::noise_position(u32 sample_time, u8 note) { double spc = samples_per_cycle(note); u32 getrandom_interval = max(static_cast(spc / 2), 1); // Note that this code only works well if the processor is called for every increment of time. - if (m_transport->time() % getrandom_interval == 0) + if (sample_time % getrandom_interval == 0) last_random[note] = (get_random() / static_cast(NumericLimits::max()) - .5) * 2; return last_random[note]; } diff --git a/Userland/Libraries/LibDSP/Synthesizers.h b/Userland/Libraries/LibDSP/Synthesizers.h index 58e205c727..99b660f20f 100644 --- a/Userland/Libraries/LibDSP/Synthesizers.h +++ b/Userland/Libraries/LibDSP/Synthesizers.h @@ -50,13 +50,13 @@ private: virtual void process_impl(Signal const&, Signal&) override; double volume_from_envelope(Envelope const&) const; - double wave_position(u8 note); + double wave_position(u32 sample_time, u8 note); double samples_per_cycle(u8 note) const; - double sin_position(u8 note) const; - double triangle_position(u8 note) const; - double square_position(u8 note) const; - double saw_position(u8 note) const; - double noise_position(u8 note); + double sin_position(u32 sample_time, u8 note) const; + double triangle_position(u32 sample_time, u8 note) const; + double square_position(u32 sample_time, u8 note) const; + double saw_position(u32 sample_time, u8 note) const; + double noise_position(u32 sample_time, u8 note); double get_random_from_seed(u64 note); ProcessorEnumParameter m_waveform; diff --git a/Userland/Libraries/LibDSP/Track.cpp b/Userland/Libraries/LibDSP/Track.cpp index f0e33c031d..7a99ba48af 100644 --- a/Userland/Libraries/LibDSP/Track.cpp +++ b/Userland/Libraries/LibDSP/Track.cpp @@ -4,6 +4,8 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include "AK/NonnullRefPtr.h" +#include "AK/Userspace.h" #include #include #include @@ -41,6 +43,27 @@ bool Track::check_processor_chain_valid_with_initial_type(SignalType initial_typ return true; } +float Track::volume() const +{ + // FIXME: This is a hack until we have a Master processor + return 1.0f; +} + +void Track::set_volume(float volume) const +{ + // FIXME: This is a hack until we have a Master processor + (void)volume; +} + +NonnullRefPtr Track::synth() +{ + return static_ptr_cast(m_processor_chain.ptr_at(0)); +} +NonnullRefPtr Track::delay() +{ + return static_ptr_cast(m_processor_chain.ptr_at(1)); +} + bool AudioTrack::check_processor_chain_valid() const { return check_processor_chain_valid_with_initial_type(SignalType::Sample); @@ -61,6 +84,7 @@ void Track::current_signal(FixedArray& output_signal) { // This is real-time code. We must NEVER EVER EVER allocate. NoAllocationGuard guard; + VERIFY(m_secondary_sample_buffer.type() == SignalType::Sample); VERIFY(output_signal.size() == m_secondary_sample_buffer.get>().size()); compute_current_clips_signal(); @@ -85,34 +109,48 @@ void Track::current_signal(FixedArray& output_signal) void NoteTrack::compute_current_clips_signal() { - // Consider the entire time duration. - TODO(); + // FIXME: Handle looping properly + u32 start_time = m_transport->time(); + VERIFY(m_secondary_sample_buffer.type() == SignalType::Sample); + size_t sample_count = m_secondary_sample_buffer.get>().size(); + u32 end_time = start_time + static_cast(sample_count); - u32 time = m_transport->time(); - // Find the currently playing clip. - NoteClip* playing_clip = nullptr; + // Find the currently playing clips. + // We can't handle more than 32 playing clips at a time, but that is a ridiculous number. + Array, 32> playing_clips; + size_t playing_clips_index = 0; for (auto& clip : m_clips) { - if (clip.start() <= time && clip.end() >= time) { - playing_clip = &clip; - break; + // A clip is playing if its start time or end time fall in the current time range. + // Or, if they both enclose the current time range. + if ((clip.start() <= start_time && clip.end() >= end_time) + || (clip.start() >= start_time && clip.start() < end_time) + || (clip.end() > start_time && clip.end() <= end_time)) { + VERIFY(playing_clips_index < playing_clips.size()); + playing_clips[playing_clips_index++] = clip; } } auto& current_notes = m_current_signal.get(); - m_current_signal.get().clear_with_capacity(); + m_current_signal.get().fill({}); - if (playing_clip == nullptr) + if (playing_clips_index == 0) return; - // FIXME: performance? - for (auto const& note_list : playing_clip->notes()) { - for (auto const& note : note_list) { - if (note.on_sample >= time && note.off_sample >= time) - break; - if (note.on_sample <= time && note.off_sample >= time) - current_notes.set(note.pitch, note); + for (auto const& playing_clip : playing_clips) { + if (playing_clip.is_null()) + break; + for (auto const& note : playing_clip->notes()) { + if (note.is_playing_during(start_time, end_time)) + current_notes[note.pitch] = note; } } + + for (auto const& keyboard_note : m_keyboard->notes()) { + if (!keyboard_note.has_value() || !keyboard_note->is_playing_during(start_time, end_time)) + continue; + // Always overwrite roll notes with keyboard notes. + current_notes[keyboard_note->pitch] = keyboard_note; + } } void AudioTrack::compute_current_clips_signal() @@ -121,4 +159,23 @@ void AudioTrack::compute_current_clips_signal() TODO(); } +void NoteTrack::set_note(RollNote note) +{ + for (auto& clip : m_clips) { + if (clip.start() <= note.on_sample && clip.end() >= note.on_sample) + clip.set_note(note); + } +} + +void NoteTrack::remove_note(RollNote note) +{ + for (auto& clip : m_clips) + clip.remove_note(note); +} + +void NoteTrack::add_clip(u32 start_time, u32 end_time) +{ + m_clips.append(AK::make_ref_counted(start_time, end_time)); +} + } diff --git a/Userland/Libraries/LibDSP/Track.h b/Userland/Libraries/LibDSP/Track.h index 58d7af134c..df333df6c1 100644 --- a/Userland/Libraries/LibDSP/Track.h +++ b/Userland/Libraries/LibDSP/Track.h @@ -6,12 +6,16 @@ #pragma once +#include #include #include #include #include +#include +#include #include #include +#include namespace DSP { @@ -32,9 +36,17 @@ public: NonnullRefPtrVector const& processor_chain() const { return m_processor_chain; } NonnullRefPtr transport() const { return m_transport; } + float volume() const; + void set_volume(float volume) const; + + // FIXME: These two getters are temporary until we have dynamic processor UI + NonnullRefPtr synth(); + NonnullRefPtr delay(); + protected: - Track(NonnullRefPtr transport) + Track(NonnullRefPtr transport, NonnullRefPtr keyboard) : m_transport(move(transport)) + , m_keyboard(move(keyboard)) { } bool check_processor_chain_valid_with_initial_type(SignalType initial_type) const; @@ -44,6 +56,7 @@ protected: NonnullRefPtrVector m_processor_chain; NonnullRefPtr m_transport; + NonnullRefPtr m_keyboard; // The current signal is stored here, to prevent unnecessary reallocation. Signal m_current_signal { FixedArray {} }; @@ -58,8 +71,19 @@ class NoteTrack final : public Track { public: virtual ~NoteTrack() override = default; + NoteTrack(NonnullRefPtr transport, NonnullRefPtr keyboard) + : Track(move(transport), move(keyboard)) + { + m_current_signal = RollNotes {}; + } + bool check_processor_chain_valid() const override; - NonnullRefPtrVector const& clips() const { return m_clips; } + Span const> notes() const { return m_clips.span(); } + + void set_note(RollNote note); + void remove_note(RollNote note); + + void add_clip(u32 start_time, u32 end_time); protected: void compute_current_clips_signal() override; @@ -72,6 +96,11 @@ class AudioTrack final : public Track { public: virtual ~AudioTrack() override = default; + AudioTrack(NonnullRefPtr transport, NonnullRefPtr keyboard) + : Track(move(transport), move(keyboard)) + { + } + bool check_processor_chain_valid() const override; NonnullRefPtrVector const& clips() const { return m_clips; }