1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 14:38:11 +00:00

Piano+LibDSP: Move Track to LibDSP

This is a tangly commit and it fixes all the bugs that a plain move
would have caused (i.e. we need to touch other logic which had wrong
assumptions).
This commit is contained in:
kleines Filmröllchen 2022-07-13 12:44:19 +02:00 committed by Linus Groh
parent 125122a9ab
commit 4941cffdd0
29 changed files with 322 additions and 413 deletions

View file

@ -15,17 +15,9 @@
#include <LibAudio/Sample.h> #include <LibAudio/Sample.h>
#include <LibCore/EventLoop.h> #include <LibCore/EventLoop.h>
static FixedArray<Audio::Sample> music_samples_to_buffer(Vector<Music::Sample>& music_samples)
{
FixedArray<Audio::Sample> samples = MUST(FixedArray<Audio::Sample>::try_create(music_samples.size()));
for (size_t i = 0; i < music_samples.size(); ++i)
samples[i] = { static_cast<float>(music_samples[i].left) / AK::NumericLimits<i16>::max(), static_cast<float>(music_samples[i].right) / AK::NumericLimits<i16>::max() };
return samples;
}
AudioPlayerLoop::AudioPlayerLoop(TrackManager& track_manager, bool& need_to_write_wav, Audio::WavWriter& wav_writer) AudioPlayerLoop::AudioPlayerLoop(TrackManager& track_manager, bool& need_to_write_wav, Audio::WavWriter& wav_writer)
: m_track_manager(track_manager) : m_track_manager(track_manager)
, m_buffer(FixedArray<DSP::Sample>::must_create_but_fixme_should_propagate_errors(sample_count))
, m_need_to_write_wav(need_to_write_wav) , m_need_to_write_wav(need_to_write_wav)
, m_wav_writer(wav_writer) , 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(); auto target_sample_rate = m_audio_client->get_sample_rate();
if (target_sample_rate == 0) if (target_sample_rate == 0)
target_sample_rate = Music::sample_rate; target_sample_rate = Music::sample_rate;
m_resampler = Audio::ResampleHelper<Sample>(Music::sample_rate, target_sample_rate); m_resampler = Audio::ResampleHelper<DSP::Sample>(Music::sample_rate, target_sample_rate);
// FIXME: I said I would never write such a hack again, but here we are. // 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. // 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&) void AudioPlayerLoop::timer_event(Core::TimerEvent&)
{ {
if (m_audio_client->remaining_samples() < buffer_size) if (m_audio_client->remaining_samples() < sample_count)
enqueue_audio(); enqueue_audio();
} }
@ -53,8 +45,7 @@ void AudioPlayerLoop::enqueue_audio()
m_track_manager.fill_buffer(m_buffer); m_track_manager.fill_buffer(m_buffer);
// FIXME: Handle OOM better. // FIXME: Handle OOM better.
auto audio_buffer = m_resampler->resample(m_buffer); auto audio_buffer = m_resampler->resample(m_buffer);
auto real_buffer = music_samples_to_buffer(audio_buffer); (void)m_audio_client->async_enqueue(audio_buffer);
(void)m_audio_client->async_enqueue(real_buffer);
// FIXME: This should be done somewhere else. // FIXME: This should be done somewhere else.
if (m_need_to_write_wav) { if (m_need_to_write_wav) {
@ -63,7 +54,7 @@ void AudioPlayerLoop::enqueue_audio()
m_track_manager.set_should_loop(false); m_track_manager.set_should_loop(false);
do { do {
m_track_manager.fill_buffer(m_buffer); m_track_manager.fill_buffer(m_buffer);
m_wav_writer.write_samples(reinterpret_cast<u8*>(m_buffer.data()), buffer_size); m_wav_writer.write_samples(m_buffer.span());
} while (m_track_manager.transport()->time()); } while (m_track_manager.transport()->time());
m_track_manager.reset(); m_track_manager.reset();
m_track_manager.set_should_loop(true); m_track_manager.set_should_loop(true);

View file

@ -14,6 +14,7 @@
#include <LibAudio/WavWriter.h> #include <LibAudio/WavWriter.h>
#include <LibCore/Event.h> #include <LibCore/Event.h>
#include <LibCore/Object.h> #include <LibCore/Object.h>
#include <LibDSP/Music.h>
class TrackManager; class TrackManager;
@ -33,8 +34,8 @@ private:
virtual void timer_event(Core::TimerEvent&) override; virtual void timer_event(Core::TimerEvent&) override;
TrackManager& m_track_manager; TrackManager& m_track_manager;
Array<Sample, sample_count> m_buffer; FixedArray<DSP::Sample> m_buffer;
Optional<Audio::ResampleHelper<Sample>> m_resampler; Optional<Audio::ResampleHelper<DSP::Sample>> m_resampler;
RefPtr<Audio::ConnectionToServer> m_audio_client; RefPtr<Audio::ConnectionToServer> m_audio_client;
bool m_should_play_audio = true; bool m_should_play_audio = true;

View file

@ -7,7 +7,6 @@ serenity_component(
set(SOURCES set(SOURCES
AudioPlayerLoop.cpp AudioPlayerLoop.cpp
Track.cpp
TrackManager.cpp TrackManager.cpp
KeysWidget.cpp KeysWidget.cpp
KnobsWidget.cpp KnobsWidget.cpp

View file

@ -34,7 +34,7 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget)
m_values_container->set_layout<GUI::HorizontalBoxLayout>(); m_values_container->set_layout<GUI::HorizontalBoxLayout>();
m_values_container->set_fixed_height(10); m_values_container->set_fixed_height(10);
m_volume_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().volume())); m_volume_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track()->volume()));
m_octave_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.keyboard()->virtual_keyboard_octave())); m_octave_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.keyboard()->virtual_keyboard_octave()));
m_knobs_container = add<GUI::Widget>(); m_knobs_container = add<GUI::Widget>();
@ -44,13 +44,13 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget)
m_volume_knob = m_knobs_container->add<GUI::VerticalSlider>(); m_volume_knob = m_knobs_container->add<GUI::VerticalSlider>();
m_volume_knob->set_range(0, volume_max); 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->set_step(10);
m_volume_knob->on_change = [this](int value) { m_volume_knob->on_change = [this](int value) {
int new_volume = volume_max - value; int new_volume = volume_max - value;
if (m_change_underlying) if (m_change_underlying)
m_track_manager.current_track().set_volume(new_volume); m_track_manager.current_track()->set_volume(new_volume);
VERIFY(new_volume == m_track_manager.current_track().volume()); VERIFY(new_volume == m_track_manager.current_track()->volume());
m_volume_value->set_text(String::number(new_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)); 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 // The synth has range and enum parameters
switch (raw_parameter.type()) { switch (raw_parameter.type()) {
case DSP::ParameterType::Range: { 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. // FIXME: We shouldn't do that, but we know the effect and it's nice.
auto& parameter = static_cast<DSP::ProcessorRangeParameter&>(raw_parameter); auto& parameter = static_cast<DSP::ProcessorRangeParameter&>(raw_parameter);
m_delay_values.append(m_values_container->add<GUI::Label>(String::number(static_cast<double>(parameter.value())))); m_delay_values.append(m_values_container->add<GUI::Label>(String::number(static_cast<double>(parameter.value()))));
@ -116,7 +116,7 @@ void KnobsWidget::update_knobs()
// need to change the slider without changing the underlying value. // need to change the slider without changing the underlying value.
m_change_underlying = false; 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_octave_knob->set_value(octave_max - m_track_manager.keyboard()->virtual_keyboard_octave());
m_change_underlying = true; m_change_underlying = true;

View file

@ -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 // 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 sample_count = 1 << 10;
constexpr int buffer_size = sample_count * sizeof(Sample);
constexpr double sample_rate = 44100; constexpr double sample_rate = 44100;
// Headroom for the synth // Headroom for the synth

View file

@ -35,9 +35,9 @@ public:
auto value = static_cast<EnumT>(model_index.row()); auto value = static_cast<EnumT>(model_index.row());
m_parameter.set_value_sneaky(value, DSP::Detail::ProcessorParameterSetValueTag {}); 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<int>(new_value)); set_selected_index(static_cast<int>(new_value));
}; });
} }
// Release focus when escape is pressed // Release focus when escape is pressed

View file

@ -30,13 +30,16 @@ ProcessorParameterSlider::ProcessorParameterSlider(Orientation orientation, DSP:
m_value_label->set_text(String::formatted("{:.2f}", static_cast<double>(m_parameter))); m_value_label->set_text(String::formatted("{:.2f}", static_cast<double>(m_parameter)));
on_change = [this](auto value) { on_change = [this](auto value) {
if (m_currently_setting_from_ui)
return;
m_currently_setting_from_ui = true;
DSP::ParameterFixedPoint real_value; DSP::ParameterFixedPoint real_value;
real_value.raw() = value; real_value.raw() = value;
if (is_logarithmic()) if (is_logarithmic())
// FIXME: Implement exponential for fixed point // FIXME: Implement exponential for fixed point
real_value = exp(static_cast<double>(real_value)); real_value = exp2(static_cast<double>(real_value));
m_parameter.set_value_sneaky(real_value, DSP::Detail::ProcessorParameterSetValueTag {}); m_parameter.set_value(real_value);
if (m_value_label) { if (m_value_label) {
double value = static_cast<double>(m_parameter); double value = static_cast<double>(m_parameter);
String label_text = String::formatted("{:.2f}", value); String label_text = String::formatted("{:.2f}", value);
@ -47,11 +50,12 @@ ProcessorParameterSlider::ProcessorParameterSlider(Orientation orientation, DSP:
else else
m_value_label->set_text(label_text); 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()) if (!is_logarithmic())
set_value(value.raw()); set_value(value.raw());
else else
set_value(value.log2().raw()); set_value(value.log2().raw());
}; });
} }

View file

@ -29,4 +29,6 @@ protected:
private: private:
// Converts based on processor parameter boundaries. // Converts based on processor parameter boundaries.
int linear_to_logarithmic(int linear_value); int linear_to_logarithmic(int linear_value);
bool m_currently_setting_from_ui { false };
}; };

View file

@ -8,6 +8,7 @@
*/ */
#include "RollWidget.h" #include "RollWidget.h"
#include "LibGUI/Event.h"
#include "TrackManager.h" #include "TrackManager.h"
#include <AK/IntegralMath.h> #include <AK/IntegralMath.h>
#include <LibGUI/Painter.h> #include <LibGUI/Painter.h>
@ -134,9 +135,9 @@ void RollWidget::paint_event(GUI::PaintEvent& event)
painter.translate(-x_offset, -y_offset); painter.translate(-x_offset, -y_offset);
painter.translate(horizontal_note_offset_remainder, note_offset_remainder); 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 const& clip : m_track_manager.current_track()->notes()) {
int y = ((note_count - 1) - note) * note_height; for (auto const& roll_note : clip->notes()) {
for (auto roll_note : m_track_manager.current_track().roll_notes(note)) { int y = ((note_count - 1) - roll_note.pitch) * note_height;
int x = m_roll_width * (static_cast<double>(roll_note.on_sample) / roll_length); 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); 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()) 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.fill_rect(rect, note_pressed_color);
painter.draw_rect(rect, Color::Black); 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); Gfx::IntRect note_name_rect(3, y, 1, note_height);
auto note_name = note_names[note % notes_per_octave]; 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()) { if (m_note_drag_location.has_value()) {
// Clear previous note // 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) { auto get_note_x = [&](int x0) {
@ -220,8 +230,8 @@ void RollWidget::mousemove_event(GUI::MouseEvent& event)
u32 on_sample = roll_length * (static_cast<double>(min(x0, x1)) / m_num_notes); 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; 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, (u8)m_drag_note, 127 };
m_note_drag_location = RollNote { on_sample, off_sample, (u8)m_drag_note, 0 }; m_track_manager.current_track()->set_note(m_note_drag_location.value());
update(); update();
} }

View file

@ -33,36 +33,8 @@ void WaveEditor::paint_event(GUI::PaintEvent& event)
GUI::Painter painter(*this); GUI::Painter painter(*this);
painter.fill_rect(frame_inner_rect(), Color::Black); painter.fill_rect(frame_inner_rect(), Color::Black);
auto recorded_sample = m_track_manager.current_track().recorded_sample(); // FIXME: This widget currently can't display anything.
if (recorded_sample.is_empty()) return;
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) SamplerWidget::SamplerWidget(TrackManager& track_manager)

View file

@ -1,118 +0,0 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
* Copyright (c) 2021, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Track.h"
#include <AK/Forward.h>
#include <AK/Math.h>
#include <AK/NonnullRefPtr.h>
#include <AK/NumericLimits.h>
#include <LibAudio/Loader.h>
#include <LibDSP/Music.h>
#include <math.h>
Track::Track(NonnullRefPtr<DSP::Transport> transport, NonnullRefPtr<DSP::Keyboard> keyboard)
: m_transport(move(transport))
, m_delay(make_ref_counted<DSP::Effects::Delay>(m_transport))
, m_synth(make_ref_counted<DSP::Synthesizers::Classic>(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<Audio::Sample>::must_create_but_fixme_should_propagate_errors(1) };
m_synth->process(playing_notes, synthesized_sample);
auto delayed_signal = DSP::Signal { FixedArray<Audio::Sample>::must_create_but_fixme_should_propagate_errors(1) };
m_delay->process(synthesized_sample, delayed_signal);
auto delayed_sample = delayed_signal.get<FixedArray<Audio::Sample>>()[0];
// HACK: Convert to old Piano range: 16-bit int
delayed_sample *= NumericLimits<i16>::max();
delayed_sample.left = clamp(delayed_sample.left, NumericLimits<i16>::min(), NumericLimits<i16>::max());
delayed_sample.right = clamp(delayed_sample.right, NumericLimits<i16>::min(), NumericLimits<i16>::max());
// TODO: Use the master processor
delayed_sample *= static_cast<double>(m_volume) / static_cast<double>(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;
}

View file

@ -1,63 +0,0 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
* Copyright (c) 2021, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Music.h"
#include <AK/Noncopyable.h>
#include <AK/NonnullRefPtr.h>
#include <AK/SinglyLinkedList.h>
#include <LibDSP/Effects.h>
#include <LibDSP/Keyboard.h>
#include <LibDSP/Music.h>
#include <LibDSP/Synthesizers.h>
#include <LibDSP/Transport.h>
using DSP::RollNote;
using RollIter = AK::SinglyLinkedListIterator<SinglyLinkedList<RollNote>, RollNote>;
class Track {
AK_MAKE_NONCOPYABLE(Track);
AK_MAKE_NONMOVABLE(Track);
public:
Track(NonnullRefPtr<DSP::Transport>, NonnullRefPtr<DSP::Keyboard>);
~Track() = default;
Vector<Audio::Sample> const& recorded_sample() const { return m_recorded_sample; }
SinglyLinkedList<RollNote> const& roll_notes(int note) const { return m_roll_notes[note]; }
int volume() const { return m_volume; }
NonnullRefPtr<DSP::Synthesizers::Classic> synth() { return m_synth; }
NonnullRefPtr<DSP::Effects::Delay> 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<Audio::Sample> m_recorded_sample;
int m_volume;
NonnullRefPtr<DSP::Transport> m_transport;
NonnullRefPtr<DSP::Effects::Delay> m_delay;
NonnullRefPtr<DSP::Synthesizers::Classic> m_synth;
SinglyLinkedList<RollNote> m_roll_notes[note_count];
RollIter m_roll_iterators[note_count];
NonnullRefPtr<DSP::Keyboard> m_keyboard;
bool m_is_active_track { false };
};

View file

@ -8,15 +8,18 @@
*/ */
#include "TrackManager.h" #include "TrackManager.h"
#include "Applications/Piano/Music.h" #include "Music.h"
#include <AK/NonnullRefPtr.h> #include <AK/NonnullRefPtr.h>
#include <AK/TypedTransfer.h>
#include <LibDSP/Effects.h>
#include <LibDSP/Synthesizers.h>
TrackManager::TrackManager() TrackManager::TrackManager()
: m_transport(make_ref_counted<DSP::Transport>(120, 4)) : m_transport(make_ref_counted<DSP::Transport>(120, 4))
, m_keyboard(make_ref_counted<DSP::Keyboard>(m_transport)) , m_keyboard(make_ref_counted<DSP::Keyboard>(m_transport))
, m_temporary_track_buffer(FixedArray<DSP::Sample>::must_create_but_fixme_should_propagate_errors(sample_count))
{ {
add_track(); add_track();
m_tracks[m_current_track]->set_active(true);
} }
void TrackManager::time_forward(int amount) void TrackManager::time_forward(int amount)
@ -30,47 +33,38 @@ void TrackManager::time_forward(int amount)
} }
} }
void TrackManager::fill_buffer(Span<Sample> buffer) void TrackManager::fill_buffer(FixedArray<DSP::Sample>& 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) {
for (auto& track : m_tracks) track->current_signal(m_temporary_track_buffer);
track->fill_sample(buffer[i]); for (size_t i = 0; i < sample_count; ++i)
buffer[i] += m_temporary_track_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;
}
} }
memcpy(m_current_back_buffer.data(), buffer.data(), buffer_size); m_transport->set_time(m_transport->time() + sample_count);
swap(m_current_front_buffer, m_current_back_buffer); // 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() 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); 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() void TrackManager::add_track()
{ {
m_tracks.append(make<Track>(m_transport, m_keyboard)); auto new_track = make_ref_counted<DSP::NoteTrack>(m_transport, m_keyboard);
MUST(new_track->resize_internal_buffers_to(m_temporary_track_buffer.size()));
new_track->add_processor(make_ref_counted<DSP::Synthesizers::Classic>(m_transport));
new_track->add_processor(make_ref_counted<DSP::Effects::Delay>(m_transport));
new_track->add_clip(0, roll_length);
m_tracks.append(move(new_track));
} }
int TrackManager::next_track_index() const int TrackManager::next_track_index() const

View file

@ -10,13 +10,14 @@
#pragma once #pragma once
#include "Music.h" #include "Music.h"
#include "Track.h"
#include <AK/Array.h> #include <AK/Array.h>
#include <AK/FixedArray.h>
#include <AK/Noncopyable.h> #include <AK/Noncopyable.h>
#include <AK/NonnullOwnPtr.h> #include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h> #include <AK/NonnullRefPtr.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibDSP/Keyboard.h> #include <LibDSP/Keyboard.h>
#include <LibDSP/Track.h>
class TrackManager { class TrackManager {
AK_MAKE_NONCOPYABLE(TrackManager); AK_MAKE_NONCOPYABLE(TrackManager);
@ -26,16 +27,12 @@ public:
TrackManager(); TrackManager();
~TrackManager() = default; ~TrackManager() = default;
Track& current_track() { return *m_tracks[m_current_track]; } NonnullRefPtr<DSP::NoteTrack> current_track() { return *m_tracks[m_current_track]; }
Span<const Sample> buffer() const { return m_current_front_buffer; }
int track_count() { return m_tracks.size(); }; int track_count() { return m_tracks.size(); };
void set_current_track(size_t track_index) void set_current_track(size_t track_index)
{ {
VERIFY((int)track_index < track_count()); VERIFY((int)track_index < track_count());
auto old_track = m_current_track;
m_current_track = track_index; m_current_track = track_index;
m_tracks[old_track]->set_active(false);
m_tracks[m_current_track]->set_active(true);
} }
NonnullRefPtr<DSP::Transport> transport() const { return m_transport; } NonnullRefPtr<DSP::Transport> transport() const { return m_transport; }
@ -43,7 +40,7 @@ public:
// Legacy API, do not add new users. // Legacy API, do not add new users.
void time_forward(int amount); void time_forward(int amount);
void fill_buffer(Span<Sample>); void fill_buffer(FixedArray<DSP::Sample>&);
void reset(); void reset();
void set_keyboard_note(int note, DSP::Keyboard::Switch note_switch); void set_keyboard_note(int note, DSP::Keyboard::Switch note_switch);
void set_should_loop(bool b) { m_should_loop = b; } void set_should_loop(bool b) { m_should_loop = b; }
@ -51,15 +48,12 @@ public:
int next_track_index() const; int next_track_index() const;
private: private:
Vector<NonnullOwnPtr<Track>> m_tracks; Vector<NonnullRefPtr<DSP::NoteTrack>> m_tracks;
NonnullRefPtr<DSP::Transport> m_transport; NonnullRefPtr<DSP::Transport> m_transport;
NonnullRefPtr<DSP::Keyboard> m_keyboard; NonnullRefPtr<DSP::Keyboard> m_keyboard;
size_t m_current_track { 0 }; size_t m_current_track { 0 };
Array<Sample, sample_count> m_front_buffer; FixedArray<DSP::Sample> m_temporary_track_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() };
bool m_should_loop { true }; bool m_should_loop { true };
}; };

View file

@ -33,9 +33,10 @@ void WaveWidget::paint_event(GUI::PaintEvent& event)
painter.fill_rect(frame_inner_rect(), Color::Black); painter.fill_rect(frame_inner_rect(), Color::Black);
painter.translate(frame_thickness(), frame_thickness()); painter.translate(frame_thickness(), frame_thickness());
Color left_wave_color = left_wave_colors[m_track_manager.current_track().synth()->wave()]; 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()]; Color right_wave_color = right_wave_colors[m_track_manager.current_track()->synth()->wave()];
auto buffer = m_track_manager.buffer(); // FIXME: We can't get the last buffer from the track manager anymore
auto buffer = FixedArray<Music::Sample>::must_create_but_fixme_should_propagate_errors(sample_count);
double width_scale = static_cast<double>(frame_inner_rect().width()) / buffer.size(); double width_scale = static_cast<double>(frame_inner_rect().width()) / buffer.size();
int prev_x = 0; int prev_x = 0;

View file

@ -40,10 +40,18 @@ void WavWriter::set_file(StringView path)
m_finalized = false; m_finalized = false;
} }
void WavWriter::write_samples(u8 const* samples, size_t size) void WavWriter::write_samples(Span<Sample> samples)
{ {
m_data_sz += size; m_data_sz += samples.size() * sizeof(Sample);
m_file->write(samples, size);
for (auto const& sample : samples) {
// FIXME: This only really works for 16-bit samples.
u16 left = static_cast<i16>(sample.left * static_cast<float>(1 << m_bits_per_sample));
u16 right = static_cast<i16>(sample.right * static_cast<float>(1 << m_bits_per_sample));
// FIXME: This ignores endianness.
m_file->write(bit_cast<u8 const*>(&left), sizeof(u16));
m_file->write(bit_cast<u8 const*>(&right), sizeof(u16));
}
} }
void WavWriter::finalize() void WavWriter::finalize()

View file

@ -8,6 +8,7 @@
#include <AK/Noncopyable.h> #include <AK/Noncopyable.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <LibAudio/Sample.h>
#include <LibCore/File.h> #include <LibCore/File.h>
namespace Audio { namespace Audio {
@ -24,7 +25,7 @@ public:
bool has_error() const { return !m_error_string.is_null(); } bool has_error() const { return !m_error_string.is_null(); }
char const* error_string() const { return m_error_string.characters(); } char const* error_string() const { return m_error_string.characters(); }
void write_samples(u8 const* samples, size_t size); void write_samples(Span<Sample> samples);
void finalize(); // You can finalize manually or let the destructor do it. void finalize(); // You can finalize manually or let the destructor do it.
u32 sample_rate() const { return m_sample_rate; } u32 sample_rate() const { return m_sample_rate; }

View file

@ -1,9 +1,9 @@
set(SOURCES set(SOURCES
Clip.cpp Clip.cpp
Track.cpp
Effects.cpp Effects.cpp
Synthesizers.cpp Synthesizers.cpp
Keyboard.cpp Keyboard.cpp
Track.cpp
) )
serenity_lib(LibDSP dsp) serenity_lib(LibDSP dsp)

View file

@ -16,30 +16,18 @@ Sample AudioClip::sample_at(u32 time)
void NoteClip::set_note(RollNote note) void NoteClip::set_note(RollNote note)
{ {
VERIFY(note.pitch >= 0 && note.pitch < note_frequencies.size()); m_notes.remove_all_matching([&](auto const& other) {
VERIFY(note.off_sample < m_length); return other.pitch == note.pitch && other.overlaps_with(note);
VERIFY(note.length() >= 2); });
m_notes.append(note);
}
auto& notes = m_notes[note.pitch]; void NoteClip::remove_note(RollNote note)
for (auto it = notes.begin(); !it.is_end();) { {
auto iterated_note = *it; // FIXME: See header; this could be much faster with a better datastructure.
if (iterated_note.on_sample > note.off_sample) { m_notes.remove_first_matching([note](auto const& element) {
notes.insert_before(it, note); return element.on_sample == note.on_sample && element.off_sample == note.off_sample && element.pitch == note.pitch;
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);
} }
} }

View file

@ -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. // A clip is a self-contained snippet of notes or audio that can freely move inside and in between tracks.
class Clip : public RefCounted<Clip> { class Clip : public RefCounted<Clip> {
public: public:
Clip(u32 start, u32 length)
: m_start(start)
, m_length(length)
{
}
virtual ~Clip() = default; virtual ~Clip() = default;
u32 start() const { return m_start; } u32 start() const { return m_start; }
@ -23,12 +29,6 @@ public:
u32 end() const { return m_start + m_length; } u32 end() const { return m_start + m_length; }
protected: protected:
Clip(u32 start, u32 length)
: m_start(start)
, m_length(length)
{
}
u32 m_start; u32 m_start;
u32 m_length; u32 m_length;
}; };
@ -45,12 +45,24 @@ private:
class NoteClip final : public Clip { class NoteClip final : public Clip {
public: public:
void set_note(RollNote note); NoteClip(u32 start, u32 length)
: Clip(start, length)
{
}
Array<SinglyLinkedList<RollNote>, 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<RollNote const> 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: private:
Array<SinglyLinkedList<RollNote>, note_frequencies.size()> m_notes; // FIXME: Better datastructures to think about here: B-Trees or good ol' RBTrees (not very cache friendly)
Vector<RollNote> m_notes;
}; };
} }

View file

@ -20,6 +20,11 @@ Delay::Delay(NonnullRefPtr<Transport> transport)
m_parameters.append(m_delay_decay); m_parameters.append(m_delay_decay);
m_parameters.append(m_delay_time); m_parameters.append(m_delay_time);
m_parameters.append(m_dry_gain); 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() 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) 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<FixedArray<Sample>>(); auto const& samples = input_signal.get<FixedArray<Sample>>();
auto& output = output_signal.get<FixedArray<Sample>>(); auto& output = output_signal.get<FixedArray<Sample>>();
for (size_t i = 0; i < output.size(); ++i) { for (size_t i = 0; i < output.size(); ++i) {

View file

@ -16,7 +16,7 @@ void Keyboard::set_keyboard_note(u8 pitch, Keyboard::Switch note_switch)
VERIFY(pitch < note_frequencies.size()); VERIFY(pitch < note_frequencies.size());
if (note_switch == Switch::Off) { if (note_switch == Switch::Off) {
m_pressed_notes.remove(pitch); m_pressed_notes[pitch] = {};
return; return;
} }
@ -27,7 +27,7 @@ void Keyboard::set_keyboard_note(u8 pitch, Keyboard::Switch note_switch)
.velocity = NumericLimits<i8>::max(), .velocity = NumericLimits<i8>::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) void Keyboard::set_keyboard_note_in_active_octave(i8 octave_offset, Switch note_switch)
{ {
@ -57,7 +57,7 @@ ErrorOr<void> Keyboard::set_virtual_keyboard_octave(u8 octave)
bool Keyboard::is_pressed(u8 pitch) const 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 bool Keyboard::is_pressed_in_active_octave(i8 octave_offset) const

View file

@ -41,7 +41,7 @@ public:
void set_keyboard_note_in_active_octave(i8 octave_offset, Switch note_switch); void set_keyboard_note_in_active_octave(i8 octave_offset, Switch note_switch);
RollNotes const& notes() const { return m_pressed_notes; } RollNotes const& notes() const { return m_pressed_notes; }
Optional<RollNote> note_at(u8 pitch) const { return m_pressed_notes.get(pitch); } Optional<RollNote> note_at(u8 pitch) const { return m_pressed_notes[pitch]; }
bool is_pressed(u8 pitch) const; bool is_pressed(u8 pitch) const;
bool is_pressed_in_active_octave(i8 octave_offset) const; bool is_pressed_in_active_octave(i8 octave_offset) const;

View file

@ -60,6 +60,24 @@ struct RollNote {
} }
constexpr bool is_playing(u32 time) const { return on_sample <= time && time <= off_sample; } 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 { enum class SignalType : u8 {
@ -78,26 +96,6 @@ public:
} }
}; };
using RollNotes = OrderedHashMap<u8, RollNote, PerfectNoteHashTraits>;
struct Signal : public Variant<FixedArray<Sample>, RollNotes> {
using Variant::Variant;
AK_MAKE_NONCOPYABLE(Signal);
public:
Signal& operator=(Signal&&) = default;
Signal(Signal&&) = default;
ALWAYS_INLINE SignalType type() const
{
if (has<FixedArray<Sample>>())
return SignalType::Sample;
if (has<RollNotes>())
return SignalType::Note;
return SignalType::Invalid;
}
};
// Equal temperament, A = 440Hz // Equal temperament, A = 440Hz
// We calculate note frequencies relative to A4: // We calculate note frequencies relative to A4:
// 440.0 * pow(pow(2.0, 1.0 / 12.0), N) // 440.0 * pow(pow(2.0, 1.0 / 12.0), N)
@ -196,7 +194,27 @@ constexpr Array<double, 84> note_frequencies = {
3951.0664100489994, 3951.0664100489994,
}; };
using RollNotes = Array<Optional<RollNote>, note_frequencies.size()>;
constexpr size_t const notes_per_octave = 12; constexpr size_t const notes_per_octave = 12;
constexpr double const middle_c = note_frequencies[36]; constexpr double const middle_c = note_frequencies[36];
struct Signal : public Variant<FixedArray<Sample>, RollNotes> {
using Variant::Variant;
AK_MAKE_NONCOPYABLE(Signal);
public:
Signal& operator=(Signal&&) = default;
Signal(Signal&&) = default;
ALWAYS_INLINE SignalType type() const
{
if (has<FixedArray<Sample>>())
return SignalType::Sample;
if (has<RollNotes>())
return SignalType::Note;
return SignalType::Invalid;
}
};
} }

View file

@ -79,7 +79,7 @@ public:
void set_value(ParameterT value) void set_value(ParameterT value)
{ {
set_value_sneaky(value, DSP::Detail::ProcessorParameterSetValueTag {}); 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); did_change_value(value);
} }
@ -90,10 +90,15 @@ public:
m_value = value; m_value = value;
} }
Function<void(ParameterT const&)> did_change_value; // FIXME: Devise a good API for unregistering listeners.
void register_change_listener(Function<void(ParameterT const&)> listener)
{
m_change_value_listeners.append(move(listener));
}
protected: protected:
ParameterT m_value; ParameterT m_value;
Vector<Function<void(ParameterT const&)>> m_change_value_listeners;
}; };
} }

View file

@ -11,6 +11,7 @@
#include <AK/StdLibExtras.h> #include <AK/StdLibExtras.h>
#include <LibAudio/Sample.h> #include <LibAudio/Sample.h>
#include <LibDSP/Envelope.h> #include <LibDSP/Envelope.h>
#include <LibDSP/Music.h>
#include <LibDSP/Processor.h> #include <LibDSP/Processor.h>
#include <LibDSP/Synthesizers.h> #include <LibDSP/Synthesizers.h>
@ -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. // Do this for every time step and set the signal accordingly.
for (size_t sample_index = 0; sample_index < output_samples.size(); ++sample_index) { for (size_t sample_index = 0; sample_index < output_samples.size(); ++sample_index) {
Sample& out = output_samples[sample_index]; Sample& out = output_samples[sample_index];
out = {};
u32 sample_time = m_transport->time() + sample_index; u32 sample_time = m_transport->time() + sample_index;
SinglyLinkedList<PitchedEnvelope> playing_envelopes; Array<Optional<PitchedEnvelope>, note_frequencies.size()> playing_envelopes;
// "Press" the necessary notes in the internal representation, // "Press" the necessary notes in the internal representation,
// and "release" all of the others // and "release" all of the others
for (u8 i = 0; i < note_frequencies.size(); ++i) { for (u8 i = 0; i < note_frequencies.size(); ++i) {
if (auto maybe_note = in.get(i); maybe_note.has_value()) if (auto maybe_note = in[i]; maybe_note.has_value())
m_playing_notes.set(i, maybe_note.value()); m_playing_notes[i] = maybe_note;
if (m_playing_notes.contains(i)) { if (m_playing_notes[i].has_value()) {
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()); 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: // 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. // 1. The envelope has expired, regardless of whether the note was still given to us in the input.
if (!note_envelope.is_active()) { if (!note_envelope.is_active()) {
m_playing_notes.remove(i); m_playing_notes[i] = {};
continue; continue;
} }
// 2. The envelope has not expired, but the note was not given to us. // 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. // 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. // 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); // dbgln("note {} not released, setting release phase, envelope={}", i, note_envelope.envelope);
note_envelope.set_release(0); 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; 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) { for (auto envelope : playing_envelopes) {
double volume = volume_from_envelope(envelope); if (!envelope.has_value())
double wave = wave_position(envelope.note); continue;
double volume = volume_from_envelope(*envelope);
double wave = wave_position(sample_time, envelope->note);
out += volume * wave; out += volume * wave;
} }
} }
@ -100,19 +104,19 @@ double Classic::volume_from_envelope(Envelope const& envelope) const
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
double Classic::wave_position(u8 note) double Classic::wave_position(u32 sample_time, u8 note)
{ {
switch (m_waveform) { switch (m_waveform) {
case Sine: case Sine:
return sin_position(note); return sin_position(sample_time, note);
case Triangle: case Triangle:
return triangle_position(note); return triangle_position(sample_time, note);
case Square: case Square:
return square_position(note); return square_position(sample_time, note);
case Saw: case Saw:
return saw_position(note); return saw_position(sample_time, note);
case Noise: case Noise:
return noise_position(note); return noise_position(sample_time, note);
} }
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
@ -122,43 +126,43 @@ double Classic::samples_per_cycle(u8 note) const
return m_transport->sample_rate() / note_frequencies[note]; 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 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<double>); return AK::sin(cycle_pos * 2 * AK::Pi<double>);
} }
// Absolute value of the saw wave "flips" the negative portion into the positive, creating a ramp up and down. // 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; return AK::fabs(saw) * 2 - 1;
} }
// The first half of the cycle period is 1, the other half -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 spc = samples_per_cycle(note);
double progress = AK::fmod(static_cast<double>(m_transport->time()), spc) / spc; double progress = AK::fmod(static_cast<double>(sample_time), spc) / spc;
return progress >= 0.5 ? -1 : 1; return progress >= 0.5 ? -1 : 1;
} }
// Modulus creates inverse saw, which we need to flip and scale. // 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 spc = samples_per_cycle(note);
double unscaled = spc - AK::fmod(static_cast<double>(m_transport->time()), spc); double unscaled = spc - AK::fmod(static_cast<double>(sample_time), spc);
return unscaled / (samples_per_cycle(note) / 2.) - 1; return unscaled / (samples_per_cycle(note) / 2.) - 1;
} }
// We resample the noise twenty times per cycle. // 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); double spc = samples_per_cycle(note);
u32 getrandom_interval = max(static_cast<u32>(spc / 2), 1); u32 getrandom_interval = max(static_cast<u32>(spc / 2), 1);
// Note that this code only works well if the processor is called for every increment of time. // 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<u16>() / static_cast<double>(NumericLimits<u16>::max()) - .5) * 2; last_random[note] = (get_random<u16>() / static_cast<double>(NumericLimits<u16>::max()) - .5) * 2;
return last_random[note]; return last_random[note];
} }

View file

@ -50,13 +50,13 @@ private:
virtual void process_impl(Signal const&, Signal&) override; virtual void process_impl(Signal const&, Signal&) override;
double volume_from_envelope(Envelope const&) const; 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 samples_per_cycle(u8 note) const;
double sin_position(u8 note) const; double sin_position(u32 sample_time, u8 note) const;
double triangle_position(u8 note) const; double triangle_position(u32 sample_time, u8 note) const;
double square_position(u8 note) const; double square_position(u32 sample_time, u8 note) const;
double saw_position(u8 note) const; double saw_position(u32 sample_time, u8 note) const;
double noise_position(u8 note); double noise_position(u32 sample_time, u8 note);
double get_random_from_seed(u64 note); double get_random_from_seed(u64 note);
ProcessorEnumParameter<Waveform> m_waveform; ProcessorEnumParameter<Waveform> m_waveform;

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "AK/NonnullRefPtr.h"
#include "AK/Userspace.h"
#include <AK/FixedArray.h> #include <AK/FixedArray.h>
#include <AK/NoAllocationGuard.h> #include <AK/NoAllocationGuard.h>
#include <AK/Optional.h> #include <AK/Optional.h>
@ -41,6 +43,27 @@ bool Track::check_processor_chain_valid_with_initial_type(SignalType initial_typ
return true; 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<Synthesizers::Classic> Track::synth()
{
return static_ptr_cast<Synthesizers::Classic>(m_processor_chain.ptr_at(0));
}
NonnullRefPtr<Effects::Delay> Track::delay()
{
return static_ptr_cast<Effects::Delay>(m_processor_chain.ptr_at(1));
}
bool AudioTrack::check_processor_chain_valid() const bool AudioTrack::check_processor_chain_valid() const
{ {
return check_processor_chain_valid_with_initial_type(SignalType::Sample); return check_processor_chain_valid_with_initial_type(SignalType::Sample);
@ -61,6 +84,7 @@ void Track::current_signal(FixedArray<Sample>& output_signal)
{ {
// This is real-time code. We must NEVER EVER EVER allocate. // This is real-time code. We must NEVER EVER EVER allocate.
NoAllocationGuard guard; NoAllocationGuard guard;
VERIFY(m_secondary_sample_buffer.type() == SignalType::Sample);
VERIFY(output_signal.size() == m_secondary_sample_buffer.get<FixedArray<Sample>>().size()); VERIFY(output_signal.size() == m_secondary_sample_buffer.get<FixedArray<Sample>>().size());
compute_current_clips_signal(); compute_current_clips_signal();
@ -85,34 +109,48 @@ void Track::current_signal(FixedArray<Sample>& output_signal)
void NoteTrack::compute_current_clips_signal() void NoteTrack::compute_current_clips_signal()
{ {
// Consider the entire time duration. // FIXME: Handle looping properly
TODO(); u32 start_time = m_transport->time();
VERIFY(m_secondary_sample_buffer.type() == SignalType::Sample);
size_t sample_count = m_secondary_sample_buffer.get<FixedArray<Sample>>().size();
u32 end_time = start_time + static_cast<u32>(sample_count);
u32 time = m_transport->time(); // Find the currently playing clips.
// Find the currently playing clip. // We can't handle more than 32 playing clips at a time, but that is a ridiculous number.
NoteClip* playing_clip = nullptr; Array<RefPtr<NoteClip>, 32> playing_clips;
size_t playing_clips_index = 0;
for (auto& clip : m_clips) { for (auto& clip : m_clips) {
if (clip.start() <= time && clip.end() >= time) { // A clip is playing if its start time or end time fall in the current time range.
playing_clip = &clip; // Or, if they both enclose the current time range.
break; 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<RollNotes>(); auto& current_notes = m_current_signal.get<RollNotes>();
m_current_signal.get<RollNotes>().clear_with_capacity(); m_current_signal.get<RollNotes>().fill({});
if (playing_clip == nullptr) if (playing_clips_index == 0)
return; return;
// FIXME: performance? for (auto const& playing_clip : playing_clips) {
for (auto const& note_list : playing_clip->notes()) { if (playing_clip.is_null())
for (auto const& note : note_list) { break;
if (note.on_sample >= time && note.off_sample >= time) for (auto const& note : playing_clip->notes()) {
break; if (note.is_playing_during(start_time, end_time))
if (note.on_sample <= time && note.off_sample >= time) current_notes[note.pitch] = note;
current_notes.set(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() void AudioTrack::compute_current_clips_signal()
@ -121,4 +159,23 @@ void AudioTrack::compute_current_clips_signal()
TODO(); 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<NoteClip>(start_time, end_time));
}
} }

View file

@ -6,12 +6,16 @@
#pragma once #pragma once
#include <AK/DisjointChunks.h>
#include <AK/NonnullRefPtrVector.h> #include <AK/NonnullRefPtrVector.h>
#include <AK/RefCounted.h> #include <AK/RefCounted.h>
#include <AK/RefPtr.h> #include <AK/RefPtr.h>
#include <LibDSP/Clip.h> #include <LibDSP/Clip.h>
#include <LibDSP/Effects.h>
#include <LibDSP/Keyboard.h>
#include <LibDSP/Music.h> #include <LibDSP/Music.h>
#include <LibDSP/Processor.h> #include <LibDSP/Processor.h>
#include <LibDSP/Synthesizers.h>
namespace DSP { namespace DSP {
@ -32,9 +36,17 @@ public:
NonnullRefPtrVector<Processor> const& processor_chain() const { return m_processor_chain; } NonnullRefPtrVector<Processor> const& processor_chain() const { return m_processor_chain; }
NonnullRefPtr<Transport const> transport() const { return m_transport; } NonnullRefPtr<Transport const> 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<Synthesizers::Classic> synth();
NonnullRefPtr<Effects::Delay> delay();
protected: protected:
Track(NonnullRefPtr<Transport> transport) Track(NonnullRefPtr<Transport> transport, NonnullRefPtr<Keyboard> keyboard)
: m_transport(move(transport)) : m_transport(move(transport))
, m_keyboard(move(keyboard))
{ {
} }
bool check_processor_chain_valid_with_initial_type(SignalType initial_type) const; bool check_processor_chain_valid_with_initial_type(SignalType initial_type) const;
@ -44,6 +56,7 @@ protected:
NonnullRefPtrVector<Processor> m_processor_chain; NonnullRefPtrVector<Processor> m_processor_chain;
NonnullRefPtr<Transport> m_transport; NonnullRefPtr<Transport> m_transport;
NonnullRefPtr<Keyboard> m_keyboard;
// The current signal is stored here, to prevent unnecessary reallocation. // The current signal is stored here, to prevent unnecessary reallocation.
Signal m_current_signal { FixedArray<Sample> {} }; Signal m_current_signal { FixedArray<Sample> {} };
@ -58,8 +71,19 @@ class NoteTrack final : public Track {
public: public:
virtual ~NoteTrack() override = default; virtual ~NoteTrack() override = default;
NoteTrack(NonnullRefPtr<Transport> transport, NonnullRefPtr<Keyboard> keyboard)
: Track(move(transport), move(keyboard))
{
m_current_signal = RollNotes {};
}
bool check_processor_chain_valid() const override; bool check_processor_chain_valid() const override;
NonnullRefPtrVector<NoteClip> const& clips() const { return m_clips; } Span<NonnullRefPtr<NoteClip> 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: protected:
void compute_current_clips_signal() override; void compute_current_clips_signal() override;
@ -72,6 +96,11 @@ class AudioTrack final : public Track {
public: public:
virtual ~AudioTrack() override = default; virtual ~AudioTrack() override = default;
AudioTrack(NonnullRefPtr<Transport> transport, NonnullRefPtr<Keyboard> keyboard)
: Track(move(transport), move(keyboard))
{
}
bool check_processor_chain_valid() const override; bool check_processor_chain_valid() const override;
NonnullRefPtrVector<AudioClip> const& clips() const { return m_clips; } NonnullRefPtrVector<AudioClip> const& clips() const { return m_clips; }