mirror of
https://github.com/RGBCube/serenity
synced 2025-07-28 01:27:44 +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:
parent
125122a9ab
commit
4941cffdd0
29 changed files with 322 additions and 413 deletions
|
@ -1,9 +1,9 @@
|
|||
set(SOURCES
|
||||
Clip.cpp
|
||||
Track.cpp
|
||||
Effects.cpp
|
||||
Synthesizers.cpp
|
||||
Keyboard.cpp
|
||||
Track.cpp
|
||||
)
|
||||
|
||||
serenity_lib(LibDSP dsp)
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Clip> {
|
||||
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<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:
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ Delay::Delay(NonnullRefPtr<Transport> 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<FixedArray<Sample>>();
|
||||
auto& output = output_signal.get<FixedArray<Sample>>();
|
||||
for (size_t i = 0; i < output.size(); ++i) {
|
||||
|
|
|
@ -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<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)
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ ErrorOr<void> 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
|
||||
|
|
|
@ -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<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_in_active_octave(i8 octave_offset) const;
|
||||
|
||||
|
|
|
@ -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<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
|
||||
// We calculate note frequencies relative to A4:
|
||||
// 440.0 * pow(pow(2.0, 1.0 / 12.0), N)
|
||||
|
@ -196,7 +194,27 @@ constexpr Array<double, 84> note_frequencies = {
|
|||
3951.0664100489994,
|
||||
};
|
||||
|
||||
using RollNotes = Array<Optional<RollNote>, note_frequencies.size()>;
|
||||
|
||||
constexpr size_t const notes_per_octave = 12;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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<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:
|
||||
ParameterT m_value;
|
||||
Vector<Function<void(ParameterT const&)>> m_change_value_listeners;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <AK/StdLibExtras.h>
|
||||
#include <LibAudio/Sample.h>
|
||||
#include <LibDSP/Envelope.h>
|
||||
#include <LibDSP/Music.h>
|
||||
#include <LibDSP/Processor.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.
|
||||
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<PitchedEnvelope> playing_envelopes;
|
||||
Array<Optional<PitchedEnvelope>, 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<double>);
|
||||
}
|
||||
|
||||
// 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<double>(m_transport->time()), spc) / spc;
|
||||
double progress = AK::fmod(static_cast<double>(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<double>(m_transport->time()), spc);
|
||||
double unscaled = spc - AK::fmod(static_cast<double>(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<u32>(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<u16>() / static_cast<double>(NumericLimits<u16>::max()) - .5) * 2;
|
||||
return last_random[note];
|
||||
}
|
||||
|
|
|
@ -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<Waveform> m_waveform;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "AK/NonnullRefPtr.h"
|
||||
#include "AK/Userspace.h"
|
||||
#include <AK/FixedArray.h>
|
||||
#include <AK/NoAllocationGuard.h>
|
||||
#include <AK/Optional.h>
|
||||
|
@ -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<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
|
||||
{
|
||||
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.
|
||||
NoAllocationGuard guard;
|
||||
VERIFY(m_secondary_sample_buffer.type() == SignalType::Sample);
|
||||
VERIFY(output_signal.size() == m_secondary_sample_buffer.get<FixedArray<Sample>>().size());
|
||||
|
||||
compute_current_clips_signal();
|
||||
|
@ -85,34 +109,48 @@ void Track::current_signal(FixedArray<Sample>& 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<FixedArray<Sample>>().size();
|
||||
u32 end_time = start_time + static_cast<u32>(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<RefPtr<NoteClip>, 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<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;
|
||||
|
||||
// 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<NoteClip>(start_time, end_time));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,12 +6,16 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/DisjointChunks.h>
|
||||
#include <AK/NonnullRefPtrVector.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <LibDSP/Clip.h>
|
||||
#include <LibDSP/Effects.h>
|
||||
#include <LibDSP/Keyboard.h>
|
||||
#include <LibDSP/Music.h>
|
||||
#include <LibDSP/Processor.h>
|
||||
#include <LibDSP/Synthesizers.h>
|
||||
|
||||
namespace DSP {
|
||||
|
||||
|
@ -32,9 +36,17 @@ public:
|
|||
NonnullRefPtrVector<Processor> const& processor_chain() const { return m_processor_chain; }
|
||||
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:
|
||||
Track(NonnullRefPtr<Transport> transport)
|
||||
Track(NonnullRefPtr<Transport> transport, NonnullRefPtr<Keyboard> 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<Processor> m_processor_chain;
|
||||
NonnullRefPtr<Transport> m_transport;
|
||||
NonnullRefPtr<Keyboard> m_keyboard;
|
||||
// The current signal is stored here, to prevent unnecessary reallocation.
|
||||
Signal m_current_signal { FixedArray<Sample> {} };
|
||||
|
||||
|
@ -58,8 +71,19 @@ class NoteTrack final : public Track {
|
|||
public:
|
||||
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;
|
||||
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:
|
||||
void compute_current_clips_signal() override;
|
||||
|
@ -72,6 +96,11 @@ class AudioTrack final : public Track {
|
|||
public:
|
||||
virtual ~AudioTrack() override = default;
|
||||
|
||||
AudioTrack(NonnullRefPtr<Transport> transport, NonnullRefPtr<Keyboard> keyboard)
|
||||
: Track(move(transport), move(keyboard))
|
||||
{
|
||||
}
|
||||
|
||||
bool check_processor_chain_valid() const override;
|
||||
NonnullRefPtrVector<AudioClip> const& clips() const { return m_clips; }
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue