diff --git a/Userland/Applications/Piano/KeysWidget.cpp b/Userland/Applications/Piano/KeysWidget.cpp index 08203a58d2..ae3124154a 100644 --- a/Userland/Applications/Piano/KeysWidget.cpp +++ b/Userland/Applications/Piano/KeysWidget.cpp @@ -42,7 +42,7 @@ void KeysWidget::set_key(int key, Switch switch_key) } VERIFY(m_key_on[key] <= 2); - m_track_manager.set_note_current_octave(key, switch_key); + m_track_manager.set_keyboard_note(key + m_track_manager.octave_base(), switch_key); } bool KeysWidget::note_is_set(int note) const diff --git a/Userland/Applications/Piano/KnobsWidget.cpp b/Userland/Applications/Piano/KnobsWidget.cpp index ed13dfe851..2e84504cd7 100644 --- a/Userland/Applications/Piano/KnobsWidget.cpp +++ b/Userland/Applications/Piano/KnobsWidget.cpp @@ -7,7 +7,9 @@ #include "KnobsWidget.h" #include "MainWidget.h" +#include "ProcessorParameterWidget/Slider.h" #include "TrackManager.h" +#include #include #include #include @@ -26,11 +28,6 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) m_volume_label = m_labels_container->add("Volume"); m_octave_label = m_labels_container->add("Octave"); - m_wave_label = m_labels_container->add("Wave"); - m_attack_label = m_labels_container->add("Attack"); - m_decay_label = m_labels_container->add("Decay"); - m_sustain_label = m_labels_container->add("Sustain"); - m_release_label = m_labels_container->add("Release"); m_values_container = add(); m_values_container->set_layout(); @@ -38,11 +35,6 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) 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.octave())); - m_wave_value = m_values_container->add(wave_strings[m_track_manager.current_track().wave()]); - m_attack_value = m_values_container->add(String::number(m_track_manager.current_track().attack())); - m_decay_value = m_values_container->add(String::number(m_track_manager.current_track().decay())); - m_sustain_value = m_values_container->add(String::number(m_track_manager.current_track().sustain())); - m_release_value = m_values_container->add(String::number(m_track_manager.current_track().release())); m_knobs_container = add(); m_knobs_container->set_layout(); @@ -74,70 +66,32 @@ KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget) m_octave_value->set_text(String::number(new_octave)); }; - m_wave_knob = m_knobs_container->add(); - m_wave_knob->set_tooltip("C: cycle through waveforms"); - m_wave_knob->set_range(0, last_wave); - m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave()); - m_wave_knob->set_step(1); - m_wave_knob->on_change = [this](int value) { - int new_wave = last_wave - value; - if (m_change_underlying) - m_track_manager.current_track().set_wave(new_wave); - VERIFY(new_wave == m_track_manager.current_track().wave()); - m_wave_value->set_text(wave_strings[new_wave]); - }; - - m_attack_knob = m_knobs_container->add(); - m_attack_knob->set_tooltip("Envelope attack in milliseconds"); - m_attack_knob->set_range(0, attack_max); - m_attack_knob->set_value(attack_max - m_track_manager.current_track().attack()); - m_attack_knob->set_step(25); - m_attack_knob->on_change = [this](int value) { - int new_attack = attack_max - value; - if (m_change_underlying) - m_track_manager.current_track().set_attack(new_attack); - VERIFY(new_attack == m_track_manager.current_track().attack()); - m_attack_value->set_text(String::number(new_attack)); - }; - - m_decay_knob = m_knobs_container->add(); - m_decay_knob->set_tooltip("Envelope decay in milliseconds"); - m_decay_knob->set_range(0, decay_max); - m_decay_knob->set_value(decay_max - m_track_manager.current_track().decay()); - m_decay_knob->set_step(25); - m_decay_knob->on_change = [this](int value) { - int new_decay = decay_max - value; - if (m_change_underlying) - m_track_manager.current_track().set_decay(new_decay); - VERIFY(new_decay == m_track_manager.current_track().decay()); - m_decay_value->set_text(String::number(new_decay)); - }; - - m_sustain_knob = m_knobs_container->add(); - m_sustain_knob->set_tooltip("Envelope sustain level percent"); - m_sustain_knob->set_range(0, sustain_max); - m_sustain_knob->set_value(sustain_max - m_track_manager.current_track().sustain()); - m_sustain_knob->set_step(25); - m_sustain_knob->on_change = [this](int value) { - int new_sustain = sustain_max - value; - if (m_change_underlying) - m_track_manager.current_track().set_sustain(new_sustain); - VERIFY(new_sustain == m_track_manager.current_track().sustain()); - m_sustain_value->set_text(String::number(new_sustain)); - }; - - m_release_knob = m_knobs_container->add(); - m_release_knob->set_tooltip("Envelope release in milliseconds"); - m_release_knob->set_range(0, release_max); - m_release_knob->set_value(release_max - m_track_manager.current_track().release()); - m_release_knob->set_step(25); - m_release_knob->on_change = [this](int value) { - int new_release = release_max - value; - if (m_change_underlying) - m_track_manager.current_track().set_release(new_release); - VERIFY(new_release == m_track_manager.current_track().release()); - m_release_value->set_text(String::number(new_release)); - }; + for (auto& raw_parameter : m_track_manager.current_track().synth()->parameters()) { + // The synth has range and enum parameters + switch (raw_parameter.type()) { + case LibDSP::ParameterType::Range: { + auto& parameter = static_cast(raw_parameter); + m_synth_values.append(m_values_container->add(String::number(static_cast(parameter.value())))); + auto& parameter_knob_value = m_synth_values.last(); + m_synth_labels.append(m_labels_container->add(String::formatted("Synth: {}", parameter.name()))); + m_synth_knobs.append(m_knobs_container->add(Orientation::Vertical, parameter, parameter_knob_value)); + break; + } + case LibDSP::ParameterType::Enum: { + // FIXME: We shouldn't do that, but we know the synth and it is nice + auto& parameter = static_cast&>(raw_parameter); + // The value is empty for enum parameters + m_synth_values.append(m_values_container->add(String::empty())); + m_synth_labels.append(m_labels_container->add(String::formatted("Synth: {}", parameter.name()))); + auto enum_strings = Vector { "Sine", "Triangle", "Square", "Saw", "Noise" }; + m_synth_knobs.append(m_knobs_container->add>(parameter, move(enum_strings))); + m_synth_waveform = static_cast&>(m_synth_knobs.last()); + break; + } + default: + VERIFY_NOT_REACHED(); + } + } 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. @@ -153,10 +107,13 @@ KnobsWidget::~KnobsWidget() { } +void KnobsWidget::cycle_waveform() +{ + m_synth_waveform->set_selected_index((m_synth_waveform->selected_index() + 1) % m_synth_waveform->model()->row_count()); +} + void KnobsWidget::update_knobs() { - m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave()); - // FIXME: This is needed because when the slider is changed normally, we // need to change the underlying value, but if the keyboard was used, we // need to change the slider without changing the underlying value. @@ -164,11 +121,6 @@ void KnobsWidget::update_knobs() m_volume_knob->set_value(volume_max - m_track_manager.current_track().volume()); m_octave_knob->set_value(octave_max - m_track_manager.octave()); - m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave()); - m_attack_knob->set_value(attack_max - m_track_manager.current_track().attack()); - m_decay_knob->set_value(decay_max - m_track_manager.current_track().decay()); - m_sustain_knob->set_value(sustain_max - m_track_manager.current_track().sustain()); - m_release_knob->set_value(release_max - m_track_manager.current_track().release()); m_change_underlying = true; } diff --git a/Userland/Applications/Piano/KnobsWidget.h b/Userland/Applications/Piano/KnobsWidget.h index 48c4c5f364..25c6add299 100644 --- a/Userland/Applications/Piano/KnobsWidget.h +++ b/Userland/Applications/Piano/KnobsWidget.h @@ -7,9 +7,14 @@ #pragma once +#include "ProcessorParameterWidget/Dropdown.h" #include "ProcessorParameterWidget/Slider.h" #include +#include +#include #include +#include +#include class TrackManager; class MainWidget; @@ -20,6 +25,7 @@ public: virtual ~KnobsWidget() override; void update_knobs(); + void cycle_waveform(); private: KnobsWidget(TrackManager&, MainWidget&); @@ -30,31 +36,20 @@ private: RefPtr m_labels_container; RefPtr m_volume_label; RefPtr m_octave_label; - RefPtr m_wave_label; - RefPtr m_attack_label; - RefPtr m_decay_label; - RefPtr m_sustain_label; - RefPtr m_release_label; + NonnullRefPtrVector m_synth_labels; NonnullRefPtrVector m_delay_labels; RefPtr m_values_container; RefPtr m_volume_value; RefPtr m_octave_value; - RefPtr m_wave_value; - RefPtr m_attack_value; - RefPtr m_decay_value; - RefPtr m_sustain_value; - RefPtr m_release_value; + NonnullRefPtrVector m_synth_values; NonnullRefPtrVector m_delay_values; RefPtr m_knobs_container; RefPtr m_volume_knob; RefPtr m_octave_knob; - RefPtr m_wave_knob; - RefPtr m_attack_knob; - RefPtr m_decay_knob; - RefPtr m_sustain_knob; - RefPtr m_release_knob; + RefPtr> m_synth_waveform; + NonnullRefPtrVector m_synth_knobs; NonnullRefPtrVector m_delay_knobs; bool m_change_underlying { true }; diff --git a/Userland/Applications/Piano/MainWidget.cpp b/Userland/Applications/Piano/MainWidget.cpp index c789e2fad0..8e7a1a4bd2 100644 --- a/Userland/Applications/Piano/MainWidget.cpp +++ b/Userland/Applications/Piano/MainWidget.cpp @@ -117,8 +117,7 @@ void MainWidget::special_key_action(int key_code) set_octave_and_ensure_note_change(Up); break; case Key_C: - m_track_manager.current_track().set_wave(Up); - m_knobs_widget->update_knobs(); + m_knobs_widget->cycle_waveform(); break; } } diff --git a/Userland/Applications/Piano/Music.h b/Userland/Applications/Piano/Music.h index ce2c814735..c4b662db8b 100644 --- a/Userland/Applications/Piano/Music.h +++ b/Userland/Applications/Piano/Music.h @@ -38,48 +38,11 @@ enum Switch { On, }; -struct RollNote { - u32 length() const { return (off_sample - on_sample) + 1; } - - u32 on_sample; - u32 off_sample; - u8 pitch; - i8 velocity; -}; - enum Direction { Down, Up, }; -enum Wave { - Sine, - Triangle, - Square, - Saw, - Noise, - RecordedSample, -}; - -constexpr const char* wave_strings[] = { - "Sine", - "Triangle", - "Square", - "Saw", - "Noise", - "Frame", -}; - -constexpr int first_wave = Sine; -constexpr int last_wave = RecordedSample; - -enum Envelope { - Done, - Attack, - Decay, - Release, -}; - enum KeyColor { White, Black, @@ -142,6 +105,9 @@ const Color left_wave_colors[] = { }, }; +// HACK: make the display code shut up for now +constexpr int RecordedSample = 5; + const Color right_wave_colors[] = { // Sine { @@ -187,13 +153,7 @@ constexpr int black_keys_per_octave = 5; constexpr int octave_min = 1; constexpr int octave_max = 7; -// These values represent the user-side bounds, the application may use a different scale. -constexpr int attack_max = 1000; -constexpr int decay_max = 1000; -constexpr int sustain_max = 1000; -constexpr int release_max = 1000; constexpr int volume_max = 1000; -constexpr int delay_max = 8; constexpr double beats_per_minute = 60; constexpr int beats_per_bar = 4; diff --git a/Userland/Applications/Piano/RollWidget.h b/Userland/Applications/Piano/RollWidget.h index db33f2aa62..d3ce7e5375 100644 --- a/Userland/Applications/Piano/RollWidget.h +++ b/Userland/Applications/Piano/RollWidget.h @@ -10,9 +10,11 @@ #include "KeysWidget.h" #include "Music.h" +#include #include class TrackManager; +using LibDSP::RollNote; class RollWidget final : public GUI::AbstractScrollableWidget { C_OBJECT(RollWidget) diff --git a/Userland/Applications/Piano/SamplerWidget.cpp b/Userland/Applications/Piano/SamplerWidget.cpp index 14051a4538..afa910c3f9 100644 --- a/Userland/Applications/Piano/SamplerWidget.cpp +++ b/Userland/Applications/Piano/SamplerWidget.cpp @@ -89,11 +89,7 @@ SamplerWidget::SamplerWidget(TrackManager& track_manager) Optional open_path = GUI::FilePicker::get_open_filepath(window()); if (!open_path.has_value()) return; - String error_string = m_track_manager.current_track().set_recorded_sample(open_path.value()); - if (!error_string.is_empty()) { - GUI::MessageBox::show(window(), String::formatted("Failed to load WAV file: {}", error_string.characters()), "Error", GUI::MessageBox::Type::Error); - return; - } + // TODO: We don't actually load the sample. m_recorded_sample_name->set_text(open_path.value()); m_wave_editor->update(); }; diff --git a/Userland/Applications/Piano/Track.cpp b/Userland/Applications/Piano/Track.cpp index 40899e4568..0a3f176cdf 100644 --- a/Userland/Applications/Piano/Track.cpp +++ b/Userland/Applications/Piano/Track.cpp @@ -7,7 +7,9 @@ */ #include "Track.h" +#include "Music.h" #include +#include #include #include #include @@ -17,12 +19,9 @@ Track::Track(const u32& time) : m_time(time) , m_temporary_transport(LibDSP::Transport::construct(120, 4)) , m_delay(make_ref_counted(m_temporary_transport)) + , m_synth(make_ref_counted(m_temporary_transport)) { set_volume(volume_max); - set_sustain_impl(1000); - set_attack(5); - set_decay(1000); - set_release(5); } Track::~Track() @@ -31,237 +30,44 @@ Track::~Track() void Track::fill_sample(Sample& sample) { - Audio::Sample new_sample; + m_temporary_transport->time() = m_time; - for (size_t note = 0; note < note_count; ++note) { - if (!m_roll_iterators[note].is_end()) { - if (m_roll_iterators[note]->on_sample == m_time) { - set_note(note, On); - } else if (m_roll_iterators[note]->off_sample == m_time) { - set_note(note, Off); - ++m_roll_iterators[note]; - if (m_roll_iterators[note].is_end()) - m_roll_iterators[note] = m_roll_notes[note].begin(); - } - } + auto playing_notes = LibDSP::RollNotes {}; - switch (m_envelope[note]) { - case Done: - continue; - case Attack: - m_power[note] += m_attack_step; - if (m_power[note] >= 1) { - m_power[note] = 1; - m_envelope[note] = Decay; - } - break; - case Decay: - m_power[note] -= m_decay_step; - if (m_power[note] < m_sustain_level) - m_power[note] = m_sustain_level; - break; - case Release: - m_power[note] -= m_release_step[note]; - if (m_power[note] <= 0) { - m_power[note] = 0; - m_envelope[note] = Done; - continue; - } - break; - default: - VERIFY_NOT_REACHED(); + for (size_t i = 0; i < note_count; ++i) { + auto& notes_at_pitch = m_roll_notes[i]; + for (auto& note : notes_at_pitch) { + if (note.is_playing(m_time)) + playing_notes.set(i, note); } - - Audio::Sample note_sample; - switch (m_wave) { - case Wave::Sine: - note_sample = sine(note); - break; - case Wave::Saw: - note_sample = saw(note); - break; - case Wave::Square: - note_sample = square(note); - break; - case Wave::Triangle: - note_sample = triangle(note); - break; - case Wave::Noise: - note_sample = noise(note); - break; - case Wave::RecordedSample: - note_sample = recorded_sample(note); - break; - default: - VERIFY_NOT_REACHED(); - } - new_sample.left += note_sample.left * m_power[note] * NumericLimits::max() * volume_factor * (static_cast(volume()) / volume_max); - new_sample.right += note_sample.right * m_power[note] * NumericLimits::max() * volume_factor * (static_cast(volume()) / volume_max); + auto& key_at_pitch = m_keyboard_notes[i]; + if (key_at_pitch.has_value() && key_at_pitch.value().is_playing(m_time)) + playing_notes.set(i, key_at_pitch.value()); + // No need to keep non-playing keyboard notes around. + else + m_keyboard_notes[i] = {}; } - auto new_sample_dsp = LibDSP::Signal(LibDSP::Sample { new_sample.left / NumericLimits::max(), new_sample.right / NumericLimits::max() }); - auto delayed_sample = m_delay->process(new_sample_dsp).get(); + auto synthesized_sample = m_synth->process(playing_notes).get(); + auto delayed_sample = m_delay->process(synthesized_sample).get(); - new_sample.left = delayed_sample.left * NumericLimits::max(); - new_sample.right = delayed_sample.right * NumericLimits::max(); + // 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; - new_sample.left = clamp(new_sample.left, NumericLimits::min(), NumericLimits::max()); - new_sample.right = clamp(new_sample.right, NumericLimits::min(), NumericLimits::max()); - - sample.left += new_sample.left; - sample.right += new_sample.right; + sample.left += delayed_sample.left; + sample.right += delayed_sample.right; } void Track::reset() { - - memset(m_note_on, 0, sizeof(m_note_on)); - memset(m_power, 0, sizeof(m_power)); - memset(m_envelope, 0, sizeof(m_envelope)); - for (size_t note = 0; note < note_count; ++note) m_roll_iterators[note] = m_roll_notes[note].begin(); } -String Track::set_recorded_sample(StringView path) -{ - NonnullRefPtr loader = Audio::Loader::create(path); - if (loader->has_error()) - return String(loader->error_string()); - auto buffer = loader->get_more_samples(60 * loader->sample_rate()); // 1 minute maximum - if (loader->has_error()) - return String(loader->error_string()); - // Resample to Piano's internal sample rate - auto resampler = Audio::ResampleHelper(loader->sample_rate(), sample_rate); - buffer = Audio::resample_buffer(resampler, *buffer); - - if (!m_recorded_sample.is_empty()) - m_recorded_sample.clear(); - m_recorded_sample.resize(buffer->sample_count()); - - double peak = 0; - for (int i = 0; i < buffer->sample_count(); ++i) { - double left_abs = fabs(buffer->samples()[i].left); - double right_abs = fabs(buffer->samples()[i].right); - if (left_abs > peak) - peak = left_abs; - if (right_abs > peak) - peak = right_abs; - } - - if (peak) { - for (int i = 0; i < buffer->sample_count(); ++i) { - m_recorded_sample[i].left = buffer->samples()[i].left / peak; - m_recorded_sample[i].right = buffer->samples()[i].right / peak; - } - } - - return String::empty(); -} - -// All of the information for these waves is on Wikipedia. - -Audio::Sample Track::sine(size_t note) -{ - double pos = note_frequencies[note] / sample_rate; - double sin_step = pos * 2 * M_PI; - double w = sin(m_pos[note]); - m_pos[note] += sin_step; - return Audio::Sample { w }; -} - -Audio::Sample Track::saw(size_t note) -{ - double saw_step = note_frequencies[note] / sample_rate; - double t = m_pos[note]; - double w = (0.5 - (t - floor(t))) * 2; - m_pos[note] += saw_step; - return Audio::Sample { w }; -} - -Audio::Sample Track::square(size_t note) -{ - double pos = note_frequencies[note] / sample_rate; - double square_step = pos * 2 * M_PI; - double w = AK::sin(m_pos[note]) >= 0 ? 1 : -1; - m_pos[note] += square_step; - return Audio::Sample { w }; -} - -Audio::Sample Track::triangle(size_t note) -{ - double triangle_step = note_frequencies[note] / sample_rate; - double t = m_pos[note]; - double w = AK::fabs(AK::fmod((4 * t) + 1, 4.) - 2) - 1.; - m_pos[note] += triangle_step; - return Audio::Sample { w }; -} - -Audio::Sample Track::noise(size_t note) -{ - double step = note_frequencies[note] / sample_rate; - // m_pos keeps track of the time since the last random sample - m_pos[note] += step; - if (m_pos[note] > 0.05) { - double random_percentage = static_cast(rand()) / RAND_MAX; - m_last_w[note] = (random_percentage * 2) - 1; - m_pos[note] = 0; - } - return Audio::Sample { m_last_w[note] }; -} - -Audio::Sample Track::recorded_sample(size_t note) -{ - int t = m_pos[note]; - if (t >= static_cast(m_recorded_sample.size())) - return {}; - double w_left = m_recorded_sample[t].left; - double w_right = m_recorded_sample[t].right; - if (t + 1 < static_cast(m_recorded_sample.size())) { - double t_fraction = m_pos[note] - t; - w_left += (m_recorded_sample[t + 1].left - m_recorded_sample[t].left) * t_fraction; - w_right += (m_recorded_sample[t + 1].right - m_recorded_sample[t].right) * t_fraction; - } - double recorded_sample_step = note_frequencies[note] / middle_c; - m_pos[note] += recorded_sample_step; - return { w_left, w_right }; -} - -static inline double calculate_step(double distance, int milliseconds) -{ - if (milliseconds == 0) - return distance; - - constexpr double samples_per_millisecond = sample_rate / 1000.0; - double samples = milliseconds * samples_per_millisecond; - double step = distance / samples; - return step; -} - -void Track::set_note(int note, Switch switch_note) -{ - VERIFY(note >= 0 && note < note_count); - - if (switch_note == On) { - if (m_note_on[note] == 0) { - m_pos[note] = 0; - m_envelope[note] = Attack; - } - ++m_note_on[note]; - } else { - if (m_note_on[note] >= 1) { - if (m_note_on[note] == 1) { - m_release_step[note] = calculate_step(m_power[note], m_release); - m_envelope[note] = Release; - } - --m_note_on[note]; - } - } - - VERIFY(m_note_on[note] != NumericLimits::max()); - VERIFY(m_power[note] >= 0); -} - void Track::sync_roll(int note) { auto it = m_roll_notes[note].find_if([&](auto& roll_note) { return roll_note.off_sample > m_time; }); @@ -286,15 +92,11 @@ void Track::set_roll_note(int note, u32 on_sample, u32 off_sample) return; } if (it->on_sample <= new_roll_note.on_sample && it->off_sample >= new_roll_note.on_sample) { - if (m_time >= it->on_sample && m_time <= it->off_sample) - set_note(note, Off); 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) { - if (m_time >= new_roll_note.off_sample && m_time <= it->off_sample) - set_note(note, Off); it.remove(m_roll_notes[note]); it = m_roll_notes[note].begin(); continue; @@ -306,21 +108,22 @@ void Track::set_roll_note(int note, u32 on_sample, u32 off_sample) sync_roll(note); } -void Track::set_wave(int wave) +void Track::set_keyboard_note(int note, Switch state) { - VERIFY(wave >= first_wave && wave <= last_wave); - m_wave = wave; -} - -void Track::set_wave(Direction direction) -{ - if (direction == Up) { - if (++m_wave > last_wave) - m_wave = first_wave; - } else { - if (--m_wave < first_wave) - m_wave = last_wave; - } + VERIFY(note >= 0 && note < note_count); + if (state == Switch::Off) { + // If the note is playing, we need to start releasing it, otherwise just delete + if (auto& maybe_roll_note = m_keyboard_notes[note]; maybe_roll_note.has_value()) { + auto& roll_note = maybe_roll_note.value(); + if (roll_note.is_playing(m_time)) + roll_note.off_sample = m_time; + else + m_keyboard_notes[note] = {}; + } + } else + // FIXME: The end time needs to be far in the future. + m_keyboard_notes[note] + = RollNote { m_time, m_time + static_cast(sample_rate) * 10'000, static_cast(note), 0 }; } void Track::set_volume(int volume) @@ -328,36 +131,3 @@ void Track::set_volume(int volume) VERIFY(volume >= 0); m_volume = volume; } - -void Track::set_attack(int attack) -{ - VERIFY(attack >= 0); - m_attack = attack; - m_attack_step = calculate_step(1, m_attack); -} - -void Track::set_decay(int decay) -{ - VERIFY(decay >= 0); - m_decay = decay; - m_decay_step = calculate_step(1 - m_sustain_level, m_decay); -} - -void Track::set_sustain_impl(int sustain) -{ - VERIFY(sustain >= 0); - m_sustain = sustain; - m_sustain_level = sustain / 1000.0; -} - -void Track::set_sustain(int sustain) -{ - set_sustain_impl(sustain); - set_decay(m_decay); -} - -void Track::set_release(int release) -{ - VERIFY(release >= 0); - m_release = release; -} diff --git a/Userland/Applications/Piano/Track.h b/Userland/Applications/Piano/Track.h index f21fd2a460..b7e48efcae 100644 --- a/Userland/Applications/Piano/Track.h +++ b/Userland/Applications/Piano/Track.h @@ -10,10 +10,14 @@ #include "Music.h" #include +#include #include #include #include +#include +#include +using LibDSP::RollNote; using RollIter = AK::SinglyLinkedListIterator, RollNote>; class Track { @@ -26,63 +30,33 @@ public: const Vector& recorded_sample() const { return m_recorded_sample; } const SinglyLinkedList& roll_notes(int note) const { return m_roll_notes[note]; } - int wave() const { return m_wave; } int volume() const { return m_volume; } - int attack() const { return m_attack; } - int decay() const { return m_decay; } - int sustain() const { return m_sustain; } - int release() const { return m_release; } + 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_note(int note, Switch); void set_roll_note(int note, u32 on_sample, u32 off_sample); - void set_wave(int wave); - void set_wave(Direction); + void set_keyboard_note(int note, Switch state); void set_volume(int volume); - void set_attack(int attack); - void set_decay(int decay); - void set_sustain(int sustain); - void set_release(int release); private: - Audio::Sample sine(size_t note); - Audio::Sample saw(size_t note); - Audio::Sample square(size_t note); - Audio::Sample triangle(size_t note); - Audio::Sample noise(size_t note); Audio::Sample recorded_sample(size_t note); void sync_roll(int note); - void set_sustain_impl(int sustain); Vector m_recorded_sample; - u8 m_note_on[note_count] { 0 }; - double m_power[note_count] { 0 }; - double m_pos[note_count]; // Initialized lazily. - // Synths may use this to keep track of the last wave position - double m_last_w[note_count] { 0 }; - Envelope m_envelope[note_count] { Done }; - - int m_wave { first_wave }; int m_volume; - int m_attack; - double m_attack_step; - int m_decay; - double m_decay_step; - int m_sustain; - double m_sustain_level; - int m_release; - double m_release_step[note_count]; const u32& m_time; NonnullRefPtr m_temporary_transport; NonnullRefPtr m_delay; + NonnullRefPtr m_synth; SinglyLinkedList m_roll_notes[note_count]; RollIter m_roll_iterators[note_count]; + Array, note_count> m_keyboard_notes; }; diff --git a/Userland/Applications/Piano/TrackManager.cpp b/Userland/Applications/Piano/TrackManager.cpp index d0f69edf1a..5e2537c77f 100644 --- a/Userland/Applications/Piano/TrackManager.cpp +++ b/Userland/Applications/Piano/TrackManager.cpp @@ -7,6 +7,7 @@ */ #include "TrackManager.h" +#include "Applications/Piano/Music.h" TrackManager::TrackManager() { @@ -61,9 +62,9 @@ void TrackManager::reset() track->reset(); } -void TrackManager::set_note_current_octave(int note, Switch switch_note) +void TrackManager::set_keyboard_note(int note, Switch note_switch) { - current_track().set_note(note + octave_base(), switch_note); + m_tracks[m_current_track]->set_keyboard_note(note, note_switch); } void TrackManager::set_octave(Direction direction) diff --git a/Userland/Applications/Piano/TrackManager.h b/Userland/Applications/Piano/TrackManager.h index 9986085f63..08893cc22f 100644 --- a/Userland/Applications/Piano/TrackManager.h +++ b/Userland/Applications/Piano/TrackManager.h @@ -33,8 +33,8 @@ public: void fill_buffer(Span); void reset(); + void set_keyboard_note(int note, Switch note_switch); void set_should_loop(bool b) { m_should_loop = b; } - void set_note_current_octave(int note, Switch); void set_octave(Direction); void set_octave(int octave); void add_track(); diff --git a/Userland/Applications/Piano/WaveWidget.cpp b/Userland/Applications/Piano/WaveWidget.cpp index 064d05029e..c91fefcfd9 100644 --- a/Userland/Applications/Piano/WaveWidget.cpp +++ b/Userland/Applications/Piano/WaveWidget.cpp @@ -36,8 +36,8 @@ 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().wave()]; - Color right_wave_color = right_wave_colors[m_track_manager.current_track().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()]; auto buffer = m_track_manager.buffer(); double width_scale = static_cast(frame_inner_rect().width()) / buffer.size(); diff --git a/Userland/Libraries/LibDSP/ProcessorParameter.h b/Userland/Libraries/LibDSP/ProcessorParameter.h index 022b24ba38..d72b74978c 100644 --- a/Userland/Libraries/LibDSP/ProcessorParameter.h +++ b/Userland/Libraries/LibDSP/ProcessorParameter.h @@ -17,18 +17,30 @@ namespace LibDSP { using ParameterFixedPoint = FixedPoint<8, i64>; +// Identifies the different kinds of parameters. +// Note that achieving parameter type identification is NOT possible with typeid(). +enum class ParameterType : u8 { + Invalid = 0, + Range, + Enum, + Boolean, +}; + // Processors have modifiable parameters that should be presented to the UI in a uniform way without requiring the processor itself to implement custom interfaces. class ProcessorParameter { public: - ProcessorParameter(String name) + ProcessorParameter(String name, ParameterType type) : m_name(move(name)) + , m_type(type) { } String const& name() const { return m_name; } + ParameterType type() const { return m_type; } private: String const m_name; + ParameterType const m_type; }; namespace Detail { @@ -41,8 +53,8 @@ template class ProcessorParameterSingleValue : public ProcessorParameter { public: - ProcessorParameterSingleValue(String name, ParameterT initial_value) - : ProcessorParameter(move(name)) + ProcessorParameterSingleValue(String name, ParameterType type, ParameterT initial_value) + : ProcessorParameter(move(name), type) , m_value(move(initial_value)) { } @@ -82,7 +94,7 @@ protected: class ProcessorBooleanParameter final : public Detail::ProcessorParameterSingleValue { public: ProcessorBooleanParameter(String name, bool initial_value) - : Detail::ProcessorParameterSingleValue(move(name), move(initial_value)) + : Detail::ProcessorParameterSingleValue(move(name), ParameterType::Boolean, move(initial_value)) { } }; @@ -90,7 +102,7 @@ public: class ProcessorRangeParameter final : public Detail::ProcessorParameterSingleValue { public: ProcessorRangeParameter(String name, ParameterFixedPoint min_value, ParameterFixedPoint max_value, ParameterFixedPoint initial_value) - : Detail::ProcessorParameterSingleValue(move(name), move(initial_value)) + : Detail::ProcessorParameterSingleValue(move(name), ParameterType::Range, move(initial_value)) , m_min_value(move(min_value)) , m_max_value(move(max_value)) , m_default_value(move(initial_value)) @@ -122,7 +134,7 @@ template requires(IsEnum) class ProcessorEnumParameter final : public Detail::ProcessorParameterSingleValue { public: ProcessorEnumParameter(String name, EnumT initial_value) - : Detail::ProcessorParameterSingleValue(move(name), initial_value, ParameterType::Enum) + : Detail::ProcessorParameterSingleValue(move(name), ParameterType::Enum, initial_value) { } }; diff --git a/Userland/Libraries/LibDSP/Synthesizers.cpp b/Userland/Libraries/LibDSP/Synthesizers.cpp index 98e97402a1..4a3988d067 100644 --- a/Userland/Libraries/LibDSP/Synthesizers.cpp +++ b/Userland/Libraries/LibDSP/Synthesizers.cpp @@ -63,6 +63,7 @@ Signal Classic::process_impl(Signal const& input_signal) return out; } +// Linear ADSR envelope with no peak adjustment. double Classic::volume_from_envelope(Envelope envelope) { switch (static_cast(envelope)) { @@ -71,10 +72,12 @@ double Classic::volume_from_envelope(Envelope envelope) case EnvelopeState::Attack: return envelope.attack(); case EnvelopeState::Decay: - return (1. - envelope.decay()) * m_sustain + m_sustain; + // As we fade from high (1) to low (headroom above the sustain level) here, use 1-decay as the interpolation. + return (1. - envelope.decay()) * (1. - m_sustain) + m_sustain; case EnvelopeState::Sustain: return m_sustain; case EnvelopeState::Release: + // Same goes for the release fade from high to low. return (1. - envelope.release()) * m_sustain; } VERIFY_NOT_REACHED();