From 4e3de71daf1fc66b4ee2cb5a716bd1535b9a9251 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Mon, 13 Feb 2023 11:50:01 +0100 Subject: [PATCH 01/17] DEVICES: blepsynth: add Sallen-Key Filter Signed-off-by: Stefan Westerfeld --- devices/blepsynth/Makefile.mk | 1 + devices/blepsynth/blepsynth.cc | 83 +++-- devices/blepsynth/skfilter.hh | 573 +++++++++++++++++++++++++++++++++ 3 files changed, 633 insertions(+), 24 deletions(-) create mode 100644 devices/blepsynth/skfilter.hh diff --git a/devices/blepsynth/Makefile.mk b/devices/blepsynth/Makefile.mk index 57fb4289..ca233a71 100644 --- a/devices/blepsynth/Makefile.mk +++ b/devices/blepsynth/Makefile.mk @@ -4,3 +4,4 @@ devices/4ase.ccfiles += $(strip \ devices/blepsynth/bleposcdata.cc \ devices/blepsynth/blepsynth.cc \ ) +$>/devices/blepsynth/blepsynth.o: EXTRA_FLAGS ::= -I/home/stefan/src/pandaresampler/lib diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index eae9d5b1..abe4eec1 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -3,6 +3,7 @@ #include "ase/midievent.hh" #include "devices/blepsynth/bleposc.hh" #include "devices/blepsynth/laddervcf.hh" +#include "devices/blepsynth/skfilter.hh" #include "devices/blepsynth/linearsmooth.hh" #include "ase/internal.hh" @@ -218,7 +219,9 @@ class BlepSynth : public AudioProcessor { ParamId pid_resonance_; ParamId pid_drive_; ParamId pid_key_track_; - ParamId pid_mode_; + ParamId pid_filter_type_; + ParamId pid_ladder_mode_; + ParamId pid_skfilter_mode_; ParamId pid_attack_; ParamId pid_decay_; @@ -258,7 +261,11 @@ class BlepSynth : public AudioProcessor { BlepUtils::OscImpl osc1_; BlepUtils::OscImpl osc2_; + + static constexpr int SKF_OVERSAMPLE = 4; + LadderVCFNonLinear vcf_; + SKFilter skfilter_ { SKF_OVERSAMPLE }; }; std::vector voices_; std::vector active_voices_; @@ -298,13 +305,37 @@ class BlepSynth : public AudioProcessor { pid_resonance_ = add_param ("Resonance", "Reso", 0, 100, 25.0, "%"); pid_drive_ = add_param ("Drive", "Drive", -24, 36, 0, "dB"); pid_key_track_ = add_param ("Key Tracking", "KeyTr", 0, 100, 50, "%"); - ChoiceS choices; - choices += { "—"_uc, "Bypass Filter" }; - choices += { "L1"_uc, "1 Pole Lowpass, 6dB/Octave" }; - choices += { "L2"_uc, "2 Pole Lowpass, 12dB/Octave" }; - choices += { "L3"_uc, "3 Pole Lowpass, 18dB/Octave" }; - choices += { "L4"_uc, "4 Pole Lowpass, 24dB/Octave" }; - pid_mode_ = add_param ("Filter Mode", "Mode", std::move (choices), 2, "", "Ladder Filter Mode to be used"); + ChoiceS filter_type_choices; + filter_type_choices += { "—"_uc, "Bypass Filter" }; + filter_type_choices += { "LD"_uc, "Ladder Filter" }; + filter_type_choices += { "SKF"_uc, "Sallen-Key Filter" }; + pid_filter_type_ = add_param ("Filter Type", "Type", std::move (filter_type_choices), 1, "", "Filter Type to be used"); + + ChoiceS ladder_mode_choices; + ladder_mode_choices += { "LP1"_uc, "1 Pole Lowpass, 6dB/Octave" }; + ladder_mode_choices += { "LP2"_uc, "2 Pole Lowpass, 12dB/Octave" }; + ladder_mode_choices += { "LP3"_uc, "3 Pole Lowpass, 18dB/Octave" }; + ladder_mode_choices += { "LP4"_uc, "4 Pole Lowpass, 24dB/Octave" }; + pid_ladder_mode_ = add_param ("Filter Mode", "Mode", std::move (ladder_mode_choices), 2, "", "Ladder Filter Mode to be used"); + + ChoiceS skfilter_mode_choices; + skfilter_mode_choices += { "LP1"_uc, "1 Pole Lowpass, 6dB/Octave" }; + skfilter_mode_choices += { "LP2"_uc, "2 Pole Lowpass, 12dB/Octave" }; + skfilter_mode_choices += { "LP3"_uc, "3 Pole Lowpass, 18dB/Octave" }; + skfilter_mode_choices += { "LP4"_uc, "4 Pole Lowpass, 24dB/Octave" }; + skfilter_mode_choices += { "LP6"_uc, "6 Pole Lowpass, 36dB/Octave" }; + skfilter_mode_choices += { "LP8"_uc, "8 Pole Lowpass, 48dB/Octave" }; + skfilter_mode_choices += { "BP2"_uc, "2 Pole Bandpass, 6dB/Octave" }; + skfilter_mode_choices += { "BP4"_uc, "4 Pole Bandpass, 12dB/Octave" }; + skfilter_mode_choices += { "BP6"_uc, "6 Pole Bandpass, 18dB/Octave" }; + skfilter_mode_choices += { "BP8"_uc, "8 Pole Bandpass, 24dB/Octave" }; + skfilter_mode_choices += { "HP1"_uc, "1 Pole Highpass, 6dB/Octave" }; + skfilter_mode_choices += { "HP2"_uc, "2 Pole Highpass, 12dB/Octave" }; + skfilter_mode_choices += { "HP3"_uc, "3 Pole Highpass, 18dB/Octave" }; + skfilter_mode_choices += { "HP4"_uc, "4 Pole Highpass, 24dB/Octave" }; + skfilter_mode_choices += { "HP6"_uc, "6 Pole Highpass, 36dB/Octave" }; + skfilter_mode_choices += { "HP8"_uc, "8 Pole Highpass, 48dB/Octave" }; + pid_skfilter_mode_ = add_param ("SKFilter Mode", "Mode", std::move (skfilter_mode_choices), 3, "", "Sallen-Key Filter Mode to be used"); oscparams (1); @@ -474,6 +505,10 @@ class BlepSynth : public AudioProcessor { voice->vcf_.reset(); voice->vcf_.set_rate (sample_rate()); + voice->skfilter_.reset(); + voice->skfilter_.set_rate (sample_rate()); + voice->skfilter_.set_frequency_range (10, 30000); + voice->cutoff_smooth_.reset (sample_rate(), 0.020); voice->last_cutoff_ = -5000; // force reset @@ -568,18 +603,15 @@ class BlepSynth : public AudioProcessor { mix_left_out[i] = osc1_left_out[i] * v1 + osc2_left_out[i] * v2; mix_right_out[i] = osc1_right_out[i] * v1 + osc2_right_out[i] * v2; } - bool run_filter = true; - switch (irintf (get_param (pid_mode_))) + switch (irintf (get_param (pid_ladder_mode_))) { - case 4: voice->vcf_.set_mode (LadderVCFMode::LP4); + case 3: voice->vcf_.set_mode (LadderVCFMode::LP4); break; - case 3: voice->vcf_.set_mode (LadderVCFMode::LP3); + case 2: voice->vcf_.set_mode (LadderVCFMode::LP3); break; - case 2: voice->vcf_.set_mode (LadderVCFMode::LP2); + case 1: voice->vcf_.set_mode (LadderVCFMode::LP2); break; - case 1: voice->vcf_.set_mode (LadderVCFMode::LP1); - break; - default: run_filter = false; + case 0: voice->vcf_.set_mode (LadderVCFMode::LP1); break; } /* --------- run ladder filter - processing in place is ok --------- */ @@ -617,17 +649,20 @@ class BlepSynth : public AudioProcessor { float freq_in[n_frames]; for (uint i = 0; i < n_frames; i++) freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); - voice->vcf_.set_drive (get_param (pid_drive_)); - float no_out[n_frames]; - if (!run_filter) + int filter_type = irintf (get_param (pid_filter_type_)); + if (filter_type == 1) + { + voice->vcf_.set_drive (get_param (pid_drive_)); + voice->vcf_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); + } + else if (filter_type == 2) { - // we keep running the filter even if it is disabled in order to have - // sane filter signal to switch to when the filter is enabled again - outputs[0] = no_out; - outputs[1] = no_out; + voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); + voice->skfilter_.set_drive (get_param (pid_drive_)); + voice->skfilter_.set_reso (resonance); + voice->skfilter_.process_block (n_frames, outputs[0], outputs[1], freq_in); } - voice->vcf_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); // apply volume envelope for (uint i = 0; i < n_frames; i++) diff --git a/devices/blepsynth/skfilter.hh b/devices/blepsynth/skfilter.hh new file mode 100644 index 00000000..fb20172e --- /dev/null +++ b/devices/blepsynth/skfilter.hh @@ -0,0 +1,573 @@ +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 + +#define PANDA_RESAMPLER_HEADER_ONLY +#include "pandaresampler.hh" +#include + +using PandaResampler::Resampler2; + +class SKFilter +{ +public: + enum Mode { + LP1, LP2, LP3, LP4, LP6, LP8, + BP2, BP4, BP6, BP8, + HP1, HP2, HP3, HP4, HP6, HP8 + }; +private: + static constexpr size_t LAST_MODE = HP8; + Mode mode_ = Mode::LP2; + float freq_ = 440; + float reso_ = 0; + float drive_ = 0; + bool test_linear_ = false; + int over_ = 1; + float freq_warp_factor_ = 0; + float frequency_range_min_ = 0; + float frequency_range_max_ = 0; + float clamp_freq_min_ = 0; + float clamp_freq_max_ = 0; + float rate_ = 0; + + static constexpr int MAX_STAGES = 4; + static constexpr uint MAX_BLOCK_SIZE = 1024; + + struct Channel + { + std::unique_ptr res_up; + std::unique_ptr res_down; + + std::array s1; + std::array s2; + }; + struct FParams + { + std::array k; + float pre_scale = 1; + float post_scale = 1; + }; + static constexpr int + mode2stages (Mode mode) + { + switch (mode) + { + case LP3: + case LP4: + case BP4: + case HP3: + case HP4: return 2; + case LP6: + case BP6: + case HP6: return 3; + case LP8: + case BP8: + case HP8: return 4; + default: return 1; + } + } + + std::array channels_; + FParams fparams_; + bool fparams_valid_ = false; + + class RTable { + std::vector res2_k; + std::vector res3_k; + std::vector res4_k; + static constexpr int TSIZE = 16; + RTable() + { + for (int order = 4; order <= 8; order += 2) + { + for (int t = 0; t <= TSIZE + 1; t++) + { + double res = std::clamp (double (t) / TSIZE, 0.0, 1.0); + + // R must be in interval [0:1] + const double R = 1 - res; + const double r_alpha = std::acos (R) / (order / 2); + + std::vector Rn; + for (int i = 0; i < order / 2; i++) + { + /* butterworth roots in s, left semi plane */ + const double bw_s_alpha = M_PI * (4 * i + order + 2) / (2 * order); + /* add resonance */ + Rn.push_back (-cos (bw_s_alpha + r_alpha)); + } + + std::sort (Rn.begin(), Rn.end(), std::greater()); + + for (auto xr : Rn) + { + if (order == 4) + res2_k.push_back ((1 - xr) * 2); + if (order == 6) + res3_k.push_back ((1 - xr) * 2); + if (order == 8) + res4_k.push_back ((1 - xr) * 2); + } + } + } + } + public: + static const RTable& + the() + { + static RTable rtable; + return rtable; + } + void + interpolate_resonance (float res, int stages, float *k, const std::vector& res_k) const + { + auto lerp = [] (float a, float b, float frac) { + return a + frac * (b - a); + }; + + float fidx = std::clamp (res, 0.f, 1.f) * TSIZE; + int idx = fidx; + float frac = fidx - idx; + + for (int s = 0; s < stages; s++) + { + k[s] = lerp (res_k[idx * stages + s], res_k[idx * stages + stages + s], frac); + } + } + void + lookup_resonance (float res, int stages, float *k) const + { + if (stages == 2) + interpolate_resonance (res, stages, k, res2_k); + + if (stages == 3) + interpolate_resonance (res, stages, k, res3_k); + + if (stages == 4) + interpolate_resonance (res, stages, k, res4_k); + + if (res > 1) + k[stages - 1] = res * 2; + } + }; + const RTable& rtable_; +public: + SKFilter (int over) : + over_ (over), + rtable_ (RTable::the()) + { + for (auto& channel : channels_) + { + channel.res_up = std::make_unique (Resampler2::UP, over_, Resampler2::PREC_72DB); + channel.res_down = std::make_unique (Resampler2::DOWN, over_, Resampler2::PREC_72DB); + } + set_rate (48000); + set_frequency_range (10, 24000); + reset(); + } +private: + void + setup_reso_drive (FParams& fparams, float reso, float drive) + { + if (test_linear_) // test filter as linear filter; don't do any resonance correction + { + const float scale = 1e-5; + fparams.pre_scale = scale; + fparams.post_scale = 1 / scale; + setup_k (fparams, reso); + + return; + } + const float db_x2_factor = 0.166096404744368; // 1/(20*log(2)/log(10)) + const float sqrt2 = M_SQRT2; + + // scale signal down (without normalization on output) for negative drive + float negative_drive_vol = 1; + if (drive < 0) + { + negative_drive_vol = exp2f (drive * db_x2_factor); + drive = 0; + } + // drive resonance boost + if (drive > 0) + reso += drive * 0.015f; + + float vol = exp2f ((drive + -18 * reso) * db_x2_factor); + + if (reso < 0.9) + { + reso = 1 - (1-reso)*(1-reso)*(1-sqrt2/4); + } + else + { + reso = 1 - (1-0.9f)*(1-0.9f)*(1-sqrt2/4) + (reso-0.9f)*0.1f; + } + + fparams.pre_scale = negative_drive_vol * vol; + fparams.post_scale = std::max (1 / vol, 1.0f); + setup_k (fparams, reso); + } + void + setup_k (FParams& fparams, float res) + { + if (mode2stages (mode_) == 1) + { + // just one stage + fparams.k[0] = res * 2; + } + else + { + rtable_.lookup_resonance (res, mode2stages (mode_), fparams.k.data()); + } + } +public: + void + set_mode (Mode m) + { + mode_ = m; + fparams_valid_ = false; + } + void + set_freq (float freq) + { + freq_ = freq; + } + void + set_reso (float reso) + { + reso_ = reso; + fparams_valid_ = false; + } + void + set_drive (float drive) + { + drive_ = drive; + fparams_valid_ = false; + } + void + set_test_linear (bool test_linear) + { + test_linear_ = test_linear; + fparams_valid_ = false; + } + void + reset () + { + for (auto& channel : channels_) + { + channel.res_up->reset(); + channel.res_down->reset(); + std::fill (channel.s1.begin(), channel.s1.end(), 0.0); + std::fill (channel.s2.begin(), channel.s2.end(), 0.0); + } + fparams_valid_ = false; + } + void + set_rate (float rate) + { + freq_warp_factor_ = 4 / (rate * over_); + rate_ = rate; + + update_frequency_range(); + } + void + set_frequency_range (float min_freq, float max_freq) + { + frequency_range_min_ = min_freq; + frequency_range_max_ = max_freq; + + update_frequency_range(); + } +private: + void + update_frequency_range() + { + /* we want to clamp to the user defined range (set_frequency_range()) + * but also enforce that the filter is well below nyquist frequency + */ + clamp_freq_min_ = frequency_range_min_; + clamp_freq_max_ = std::min (frequency_range_max_, rate_ * over_ * 0.49f); + } + float + cutoff_warp (float freq) + { + float x = freq * freq_warp_factor_; + + /* approximate tan (pi*x/4) for cutoff warping */ + const float c1 = -3.16783027; + const float c2 = 0.134516124; + const float c3 = -4.033321984; + + float x2 = x * x; + + return x * (c1 + c2 * x2) / (c3 + x2); + } + template + [[gnu::flatten]] + void + process (float *left, float *right, float freq, uint n_samples) + { + float g = cutoff_warp (std::clamp (freq, clamp_freq_min_, clamp_freq_max_)); + float G = g / (1 + g); + + for (int stage = 0; stage < mode2stages (MODE); stage++) + { + const float k = fparams_.k[stage]; + + float xnorm = 1.f / (1 - k * G + k * G * G); + float s1feedback = -xnorm * k * (G - 1) / (1 + g); + float s2feedback = -xnorm * k / (1 + g); + + auto lowpass = [G] (float in, float& state) + { + float v = G * (in - state); + float y = v + state; + state = y + v; + return y; + }; + + auto mode_out = [] (float y0, float y1, float y2, bool last_stage) -> float + { + float y1hp = y0 - y1; + float y2hp = y1 - y2; + + switch (MODE) + { + case LP2: + case LP4: + case LP6: + case LP8: return y2; + case BP2: + case BP4: + case BP6: + case BP8: return y2hp; + case HP2: + case HP4: + case HP6: + case HP8: return (y1hp - y2hp); + case LP1: + case LP3: return last_stage ? y1 : y2; + case HP1: + case HP3: return last_stage ? y1hp : (y1hp - y2hp); + } + }; + + auto distort = [] (float x) + { + /* shaped somewhat similar to tanh() and others, but faster */ + x = std::clamp (x, -1.0f, 1.0f); + + return x - x * x * x * (1.0f / 3); + }; + + float s1l, s1r, s2l, s2r; + + s1l = channels_[0].s1[stage]; + s2l = channels_[0].s2[stage]; + + if (STEREO) + { + s1r = channels_[1].s1[stage]; + s2r = channels_[1].s2[stage]; + } + + auto tick = [&] (uint i, bool last_stage, float pre_scale, float post_scale) + { + float xl, xr, y0l, y0r, y1l, y1r, y2l, y2r; + + /* + * interleaving processing of both channels performs better than + * processing left and right channel seperately (measured on Ryzen7) + */ + + { xl = left[i] * pre_scale; } + if (STEREO) { xr = right[i] * pre_scale; } + + { y0l = xl * xnorm + s1l * s1feedback + s2l * s2feedback; } + if (STEREO) { y0r = xr * xnorm + s1r * s1feedback + s2r * s2feedback; } + + if (last_stage) + { + { y0l = distort (y0l); } + if (STEREO) { y0r = distort (y0r); } + } + { y1l = lowpass (y0l, s1l); } + if (STEREO) { y1r = lowpass (y0r, s1r); } + + { y2l = lowpass (y1l, s2l); } + if (STEREO) { y2r = lowpass (y1r, s2r); } + + { left[i] = mode_out (y0l, y1l, y2l, last_stage) * post_scale; } + if (STEREO) { right[i] = mode_out (y0r, y1r, y2r, last_stage) * post_scale; } + }; + + const bool last_stage = mode2stages (MODE) == (stage + 1); + + if (last_stage) + { + for (uint i = 0; i < n_samples; i++) + tick (i, true, fparams_.pre_scale, fparams_.post_scale); + } + else + { + for (uint i = 0; i < n_samples; i++) + tick (i, false, 1, 1); + } + + channels_[0].s1[stage] = s1l; + channels_[0].s2[stage] = s2l; + + if (STEREO) + { + channels_[1].s1[stage] = s1r; + channels_[1].s2[stage] = s2r; + } + } + } + template + void + process_block_mode (uint n_samples, float *left, float *right, const float *freq_in, const float *reso_in, const float *drive_in) + { + float over_samples_left[n_samples * over_]; + float over_samples_right[n_samples * over_]; + + /* we only support stereo (left != 0, right != 0) and mono (left != 0, right == 0) */ + bool stereo = left && right; + + channels_[0].res_up->process_block (left, n_samples, over_samples_left); + if (stereo) + channels_[1].res_up->process_block (right, n_samples, over_samples_right); + + if (!fparams_valid_) + { + setup_reso_drive (fparams_, reso_in ? reso_in[0] : reso_, drive_in ? drive_in[0] : drive_); + fparams_valid_ = true; + } + + if (reso_in || drive_in) + { + /* for reso or drive modulation, we split the input it into small blocks + * and interpolate the pre_scale / post_scale / k parameters + */ + float *left_blk = over_samples_left; + float *right_blk = over_samples_right; + + uint n_remaining_samples = n_samples; + while (n_remaining_samples) + { + const uint todo = std::min (n_remaining_samples, 64); + + FParams fparams_end; + setup_reso_drive (fparams_end, reso_in ? reso_in[todo - 1] : reso_, drive_in ? drive_in[todo - 1] : drive_); + + constexpr static int STAGES = mode2stages (MODE); + float todo_inv = 1.f / todo; + float delta_pre_scale = (fparams_end.pre_scale - fparams_.pre_scale) * todo_inv; + float delta_post_scale = (fparams_end.post_scale - fparams_.post_scale) * todo_inv; + float delta_k[STAGES]; + for (int stage = 0; stage < STAGES; stage++) + delta_k[stage] = (fparams_end.k[stage] - fparams_.k[stage]) * todo_inv; + + uint j = 0; + for (uint i = 0; i < todo * over_; i += over_) + { + fparams_.pre_scale += delta_pre_scale; + fparams_.post_scale += delta_post_scale; + + for (int stage = 0; stage < STAGES; stage++) + fparams_.k[stage] += delta_k[stage]; + + float freq = freq_in ? freq_in[j++] : freq_; + + if (stereo) + { + process (left_blk + i, right_blk + i, freq, over_); + } + else + { + process (left_blk + i, nullptr, freq, over_); + } + } + + n_remaining_samples -= todo; + left_blk += todo * over_; + right_blk += todo * over_; + + if (freq_in) + freq_in += todo; + if (reso_in) + reso_in += todo; + if (drive_in) + drive_in += todo; + } + } + else if (freq_in) + { + uint j = 0; + for (uint i = 0; i < n_samples * over_; i += over_) + { + float freq = freq_in[j++]; + + if (stereo) + { + process (over_samples_left + i, over_samples_right + i, freq, over_); + } + else + { + process (over_samples_left + i, nullptr, freq, over_); + } + } + } + else + { + if (stereo) + { + process (over_samples_left, over_samples_right, freq_, n_samples * over_); + } + else + { + process (over_samples_left, nullptr, freq_, n_samples * over_); + } + } + + channels_[0].res_down->process_block (over_samples_left, n_samples * over_, left); + if (stereo) + channels_[1].res_down->process_block (over_samples_right, n_samples * over_, right); + } + + using ProcessBlockFunc = decltype (&SKFilter::process_block_mode); + + template + static constexpr std::array + make_jump_table (std::integer_sequence) + { + auto mk_func = [] (auto I) { return &SKFilter::process_block_mode; }; + + return { mk_func (std::integral_constant{})... }; + } +public: + void + process_block (uint n_samples, float *left, float *right, const float *freq_in, const float *reso_in = nullptr, const float *drive_in = nullptr) + { + static constexpr auto jump_table { make_jump_table (std::make_index_sequence()) }; + + while (n_samples) + { + const uint todo = std::min (n_samples, MAX_BLOCK_SIZE); + + (this->*jump_table[mode_]) (todo, left, right, freq_in, reso_in, drive_in); + + if (left) + left += todo; + if (right) + right += todo; + if (freq_in) + freq_in += todo; + if (reso_in) + reso_in += todo; + if (drive_in) + drive_in += todo; + + n_samples -= todo; + } + } +}; From c14c50ac08769347e678e2794a87335538481939 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Mon, 13 Feb 2023 11:59:02 +0100 Subject: [PATCH 02/17] DEVICES: blepsynth: smooth drive / reso input for Sallen-Key Filter Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 41 +++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index abe4eec1..1f2a42bf 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -259,6 +259,12 @@ class BlepSynth : public AudioProcessor { LinearSmooth cut_mod_smooth_; double last_cut_mod_; + LinearSmooth reso_smooth_; + double last_reso_; + + LinearSmooth drive_smooth_; + double last_drive_; + BlepUtils::OscImpl osc1_; BlepUtils::OscImpl osc2_; @@ -515,6 +521,12 @@ class BlepSynth : public AudioProcessor { voice->cut_mod_smooth_.reset (sample_rate(), 0.020); voice->last_cut_mod_ = -5000; // force reset voice->last_key_track_ = -5000; + + voice->reso_smooth_.reset (sample_rate(), 0.020); + voice->last_reso_ = -5000; // force reset + // + voice->drive_smooth_.reset (sample_rate(), 0.020); + voice->last_drive_ = -5000; // force reset } } void @@ -620,7 +632,6 @@ class BlepSynth : public AudioProcessor { const float *inputs[2] = { mix_left_out, mix_right_out }; float *outputs[2] = { mix_left_out, mix_right_out }; double cutoff = get_param (pid_cutoff_); - double resonance = get_param (pid_resonance_) * 0.01; double key_track = get_param (pid_key_track_) * 0.01; if (fabs (voice->last_cutoff_ - cutoff) > 1e-7 || fabs (voice->last_key_track_ - key_track) > 1e-7) @@ -642,13 +653,33 @@ class BlepSynth : public AudioProcessor { voice->cut_mod_smooth_.set (cut_mod, reset); voice->last_cut_mod_ = cut_mod; } + double resonance = get_param (pid_resonance_) * 0.01; + if (fabs (voice->last_reso_ - resonance) > 1e-7) + { + const bool reset = voice->last_reso_ < -1000; + + voice->reso_smooth_.set (resonance, reset); + voice->last_reso_ = resonance; + } + double drive = get_param (pid_drive_); + if (fabs (voice->last_drive_ - drive) > 1e-7) + { + const bool reset = voice->last_drive_ < -1000; + + voice->drive_smooth_.set (drive, reset); + voice->last_drive_ = drive; + } /* TODO: possible improvements: * - exponential smoothing (get rid of exp2f) * - don't do anything if cutoff_smooth_->steps_ == 0 (add accessor) */ - float freq_in[n_frames]; + float freq_in[n_frames], reso_in[n_frames], drive_in[n_frames]; for (uint i = 0; i < n_frames; i++) - freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); + { + freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); + reso_in[i] = voice->reso_smooth_.get_next(); + drive_in[i] = voice->drive_smooth_.get_next(); + } int filter_type = irintf (get_param (pid_filter_type_)); if (filter_type == 1) @@ -659,9 +690,7 @@ class BlepSynth : public AudioProcessor { else if (filter_type == 2) { voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); - voice->skfilter_.set_drive (get_param (pid_drive_)); - voice->skfilter_.set_reso (resonance); - voice->skfilter_.process_block (n_frames, outputs[0], outputs[1], freq_in); + voice->skfilter_.process_block (n_frames, outputs[0], outputs[1], freq_in, reso_in, drive_in); } // apply volume envelope From 0ead62c750abbe26521a79ec07adf75b74895dc7 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Mon, 13 Feb 2023 12:22:33 +0100 Subject: [PATCH 03/17] DEVICES: blepsynth: implement midi velocity handling and add master gain Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 39 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 1f2a42bf..30e2c82c 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -213,6 +213,8 @@ class BlepSynth : public AudioProcessor { }; OscParams osc_params[2]; ParamId pid_mix_; + ParamId pid_vel_track_; + ParamId pid_post_gain_; ParamId pid_cutoff_; Logscale cutoff_logscale_; @@ -251,6 +253,7 @@ class BlepSynth : public AudioProcessor { int midi_note_ = -1; int channel_ = 0; double freq_ = 0; + float vel_gain_ = 0; LinearSmooth cutoff_smooth_; double last_cutoff_; @@ -322,7 +325,7 @@ class BlepSynth : public AudioProcessor { ladder_mode_choices += { "LP2"_uc, "2 Pole Lowpass, 12dB/Octave" }; ladder_mode_choices += { "LP3"_uc, "3 Pole Lowpass, 18dB/Octave" }; ladder_mode_choices += { "LP4"_uc, "4 Pole Lowpass, 24dB/Octave" }; - pid_ladder_mode_ = add_param ("Filter Mode", "Mode", std::move (ladder_mode_choices), 2, "", "Ladder Filter Mode to be used"); + pid_ladder_mode_ = add_param ("Filter Mode", "Mode", std::move (ladder_mode_choices), 1, "", "Ladder Filter Mode to be used"); ChoiceS skfilter_mode_choices; skfilter_mode_choices += { "LP1"_uc, "1 Pole Lowpass, 6dB/Octave" }; @@ -341,7 +344,7 @@ class BlepSynth : public AudioProcessor { skfilter_mode_choices += { "HP4"_uc, "4 Pole Highpass, 24dB/Octave" }; skfilter_mode_choices += { "HP6"_uc, "6 Pole Highpass, 36dB/Octave" }; skfilter_mode_choices += { "HP8"_uc, "8 Pole Highpass, 48dB/Octave" }; - pid_skfilter_mode_ = add_param ("SKFilter Mode", "Mode", std::move (skfilter_mode_choices), 3, "", "Sallen-Key Filter Mode to be used"); + pid_skfilter_mode_ = add_param ("SKFilter Mode", "Mode", std::move (skfilter_mode_choices), 2, "", "Sallen-Key Filter Mode to be used"); oscparams (1); @@ -360,6 +363,9 @@ class BlepSynth : public AudioProcessor { start_group ("Mix"); pid_mix_ = add_param ("Mix", "Mix", 0, 100, 0, "%"); + pid_vel_track_ = add_param ("Velocity Tracking", "VelTr", 0, 100, 50, "%"); + // TODO: this probably should default to 0dB once we have track/mixer volumes + pid_post_gain_ = add_param ("Post Gain", "Gain", -24, 24, -12, "dB"); start_group ("Keyboard Input"); pid_c_ = add_param ("Main Input 1", "C", false, GUIONLY); @@ -472,8 +478,25 @@ class BlepSynth : public AudioProcessor { return string_format ("%.1f ms", ms); return string_format ("%.2f ms", ms); } + static float + velocity_to_gain (float velocity, float vel_track) + { + /* input: velocity [0..1] + * vel_track [0..1] + * + * convert, so that + * - gain (0) is (1 - vel_track)^2 + * - gain (1) is 1 + * - sqrt(gain(velocity)) is a straight line + * + * See Roger B. Dannenberg: The Interpretation of Midi Velocity + */ + const float x = (1 - vel_track) + vel_track * velocity; + + return x * x; + } void - note_on (int channel, int midi_note, int vel) + note_on (int channel, int midi_note, float vel) { Voice *voice = alloc_voice(); if (voice) @@ -482,6 +505,7 @@ class BlepSynth : public AudioProcessor { voice->state_ = Voice::ON; voice->channel_ = channel; voice->midi_note_ = midi_note; + voice->vel_gain_ = velocity_to_gain (vel, get_param (pid_vel_track_) * 0.01); // Volume Envelope /* TODO: maybe use non-linear translation between level and sustain % */ @@ -550,7 +574,7 @@ class BlepSynth : public AudioProcessor { { constexpr int channel = 0; if (value) - note_on (channel, note, 100); + note_on (channel, note, 100./127.); else note_off (channel, note); old_value = value; @@ -608,8 +632,8 @@ class BlepSynth : public AudioProcessor { float mix_left_out[n_frames]; float mix_right_out[n_frames]; const float mix_norm = get_param (pid_mix_) * 0.01; - const float v1 = 1 - mix_norm; - const float v2 = mix_norm; + const float v1 = voice->vel_gain_ * (1 - mix_norm); + const float v2 = voice->vel_gain_ * mix_norm; for (uint i = 0; i < n_frames; i++) { mix_left_out[i] = osc1_left_out[i] * v1 + osc2_left_out[i] * v2; @@ -694,9 +718,10 @@ class BlepSynth : public AudioProcessor { } // apply volume envelope + float post_gain_factor = db2voltage (get_param (pid_post_gain_)); for (uint i = 0; i < n_frames; i++) { - float amp = 0.25 * voice->envelope_.get_next(); + float amp = post_gain_factor * voice->envelope_.get_next(); left_out[i] += mix_left_out[i] * amp; right_out[i] += mix_right_out[i] * amp; } From 5e59f9cfb3db03bf59f8fac5786a81a10c4f138d Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Mon, 13 Feb 2023 12:26:08 +0100 Subject: [PATCH 04/17] DEVICES: blepsynth: rename vcf_ to ladder_filter_; minor cleanups Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 30e2c82c..3749ede3 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -273,7 +273,7 @@ class BlepSynth : public AudioProcessor { static constexpr int SKF_OVERSAMPLE = 4; - LadderVCFNonLinear vcf_; + LadderVCFNonLinear ladder_filter_; SKFilter skfilter_ { SKF_OVERSAMPLE }; }; std::vector voices_; @@ -532,8 +532,8 @@ class BlepSynth : public AudioProcessor { voice->osc1_.reset(); voice->osc2_.reset(); - voice->vcf_.reset(); - voice->vcf_.set_rate (sample_rate()); + voice->ladder_filter_.reset(); + voice->ladder_filter_.set_rate (sample_rate()); voice->skfilter_.reset(); voice->skfilter_.set_rate (sample_rate()); @@ -639,17 +639,6 @@ class BlepSynth : public AudioProcessor { mix_left_out[i] = osc1_left_out[i] * v1 + osc2_left_out[i] * v2; mix_right_out[i] = osc1_right_out[i] * v1 + osc2_right_out[i] * v2; } - switch (irintf (get_param (pid_ladder_mode_))) - { - case 3: voice->vcf_.set_mode (LadderVCFMode::LP4); - break; - case 2: voice->vcf_.set_mode (LadderVCFMode::LP3); - break; - case 1: voice->vcf_.set_mode (LadderVCFMode::LP2); - break; - case 0: voice->vcf_.set_mode (LadderVCFMode::LP1); - break; - } /* --------- run ladder filter - processing in place is ok --------- */ /* TODO: under some conditions we could enable SSE in LadderVCF (alignment and block_size) */ @@ -708,8 +697,9 @@ class BlepSynth : public AudioProcessor { int filter_type = irintf (get_param (pid_filter_type_)); if (filter_type == 1) { - voice->vcf_.set_drive (get_param (pid_drive_)); - voice->vcf_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); + voice->ladder_filter_.set_mode (LadderVCFMode (irintf (get_param (pid_ladder_mode_)))); + voice->ladder_filter_.set_drive (get_param (pid_drive_)); + voice->ladder_filter_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); } else if (filter_type == 2) { From 92f9254702945258daae78dc6fb6c522c46bc8b3 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Mon, 13 Feb 2023 12:53:48 +0100 Subject: [PATCH 05/17] DEVICES: blepsynth: make cutoff parameter log-scaled Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 41 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 3749ede3..aa21b079 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -217,7 +217,6 @@ class BlepSynth : public AudioProcessor { ParamId pid_post_gain_; ParamId pid_cutoff_; - Logscale cutoff_logscale_; ParamId pid_resonance_; ParamId pid_drive_; ParamId pid_key_track_; @@ -306,11 +305,7 @@ class BlepSynth : public AudioProcessor { start_group ("Filter"); - const double FsharpHz = 440 * ::pow (2, 9 / 12.); - const double freq_lo = FsharpHz / ::pow (2, 5); - const double freq_hi = FsharpHz * ::pow (2, 5); - pid_cutoff_ = add_param ("Cutoff", "Cutoff", freq_lo, freq_hi, FsharpHz, "Hz", STANDARD); - cutoff_logscale_.setup (freq_lo, freq_hi); + pid_cutoff_ = add_param ("Cutoff", "Cutoff", 15, 144, 60); // cutoff as midi notes pid_resonance_ = add_param ("Resonance", "Reso", 0, 100, 25.0, "%"); pid_drive_ = add_param ("Drive", "Drive", -24, 36, 0, "dB"); pid_key_track_ = add_param ("Key Tracking", "KeyTr", 0, 100, 50, "%"); @@ -478,6 +473,17 @@ class BlepSynth : public AudioProcessor { return string_format ("%.1f ms", ms); return string_format ("%.2f ms", ms); } + static String + hz_to_str (double hz) + { + if (hz > 10000) + return string_format ("%.1f kHz", hz / 1000); + if (hz > 1000) + return string_format ("%.2f kHz", hz / 1000); + if (hz > 100) + return string_format ("%.0f Hz", hz); + return string_format ("%.1f Hz", hz); + } static float velocity_to_gain (float velocity, float vel_track) { @@ -644,7 +650,7 @@ class BlepSynth : public AudioProcessor { /* TODO: under some conditions we could enable SSE in LadderVCF (alignment and block_size) */ const float *inputs[2] = { mix_left_out, mix_right_out }; float *outputs[2] = { mix_left_out, mix_right_out }; - double cutoff = get_param (pid_cutoff_); + double cutoff = convert_cutoff (get_param (pid_cutoff_)); double key_track = get_param (pid_key_track_) * 0.01; if (fabs (voice->last_cutoff_ - cutoff) > 1e-7 || fabs (voice->last_key_track_ - key_track) > 1e-7) @@ -724,6 +730,11 @@ class BlepSynth : public AudioProcessor { if (need_free) free_unused_voices(); } + static double + convert_cutoff (double midi_note) + { + return 440 * std::pow (2, (midi_note - 69) / 12.); + } std::string param_value_to_text (Id32 paramid, double value) const override { @@ -738,23 +749,11 @@ class BlepSynth : public AudioProcessor { for (auto p : { pid_attack_, pid_decay_, pid_release_, pid_fil_attack_, pid_fil_decay_, pid_fil_release_ }) if (paramid == p) return perc_to_str (value); + if (paramid == pid_cutoff_) + return hz_to_str (convert_cutoff (value)); return AudioProcessor::param_value_to_text (paramid, value); } - double - value_to_normalized (Id32 paramid, double value) const override - { - if (paramid == pid_cutoff_) - return cutoff_logscale_.iscale (value); - return AudioProcessor::value_to_normalized (paramid, value); - } - double - value_from_normalized (Id32 paramid, double normalized) const override - { - if (paramid == pid_cutoff_) - return cutoff_logscale_.scale (normalized); - return AudioProcessor::value_from_normalized (paramid, normalized); - } public: BlepSynth (AudioEngine &engine) : AudioProcessor (engine) From 66930769f54f5096080180d1f467a5a014f60f85 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Tue, 14 Feb 2023 10:45:22 +0100 Subject: [PATCH 06/17] DEVICES: blepsynth: reset SKFilter state if filter mode changes Signed-off-by: Stefan Westerfeld --- devices/blepsynth/skfilter.hh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/devices/blepsynth/skfilter.hh b/devices/blepsynth/skfilter.hh index fb20172e..a7eb4ecd 100644 --- a/devices/blepsynth/skfilter.hh +++ b/devices/blepsynth/skfilter.hh @@ -219,12 +219,26 @@ private: rtable_.lookup_resonance (res, mode2stages (mode_), fparams.k.data()); } } + void + zero_fill_state() + { + for (auto& channel : channels_) + { + std::fill (channel.s1.begin(), channel.s1.end(), 0.0); + std::fill (channel.s2.begin(), channel.s2.end(), 0.0); + } + } public: void set_mode (Mode m) { - mode_ = m; - fparams_valid_ = false; + if (mode_ != m) + { + mode_ = m; + + zero_fill_state(); + fparams_valid_ = false; + } } void set_freq (float freq) @@ -256,9 +270,8 @@ public: { channel.res_up->reset(); channel.res_down->reset(); - std::fill (channel.s1.begin(), channel.s1.end(), 0.0); - std::fill (channel.s2.begin(), channel.s2.end(), 0.0); } + zero_fill_state(); fparams_valid_ = false; } void From 864562b39df6fe9ad9fea2b85204053e6d0b465e Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Tue, 14 Feb 2023 11:01:17 +0100 Subject: [PATCH 07/17] DEVICES: blepsynth: reset filter state on filter type change Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index aa21b079..6919f5e0 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -427,6 +427,7 @@ class BlepSynth : public AudioProcessor { { set_max_voices (0); set_max_voices (32); + adjust_params (true); } void init_osc (BlepUtils::OscImpl& osc, float freq) @@ -438,6 +439,18 @@ class BlepSynth : public AudioProcessor { #endif } void + adjust_param (Id32 tag) override + { + if (tag == pid_filter_type_) + { + for (Voice *voice : active_voices_) + { + voice->ladder_filter_.reset(); + voice->skfilter_.reset(); + } + } + } + void update_osc (BlepUtils::OscImpl& osc, const OscParams& params) { osc.shape_base = get_param (params.shape) * 0.01; @@ -538,6 +551,7 @@ class BlepSynth : public AudioProcessor { voice->osc1_.reset(); voice->osc2_.reset(); + voice->ladder_filter_.reset(); voice->ladder_filter_.set_rate (sample_rate()); @@ -589,6 +603,8 @@ class BlepSynth : public AudioProcessor { void render (uint n_frames) override { + adjust_params (false); + /* TODO: replace this with true midi input */ check_note (pid_c_, old_c_, 60); check_note (pid_d_, old_d_, 62); From bfa4faf772ed0ba2eafc7725bda906e115d00961 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Tue, 14 Feb 2023 12:03:40 +0100 Subject: [PATCH 08/17] DEVICES: blepsynth: compensate for SKFilter FIR oversampling delay Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 196 ++++++++++++++++++--------------- devices/blepsynth/skfilter.hh | 5 + 2 files changed, 115 insertions(+), 86 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 6919f5e0..044210f8 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -253,6 +253,7 @@ class BlepSynth : public AudioProcessor { int channel_ = 0; double freq_ = 0; float vel_gain_ = 0; + bool new_voice_ = false; LinearSmooth cutoff_smooth_; double last_cutoff_; @@ -558,6 +559,7 @@ class BlepSynth : public AudioProcessor { voice->skfilter_.reset(); voice->skfilter_.set_rate (sample_rate()); voice->skfilter_.set_frequency_range (10, 30000); + voice->new_voice_ = true; voice->cutoff_smooth_.reset (sample_rate(), 0.020); voice->last_cutoff_ = -5000; // force reset @@ -600,6 +602,100 @@ class BlepSynth : public AudioProcessor { old_value = value; } } + template + void + render_voice (Voice *voice, uint n_frames, float *mix_left_out, float *mix_right_out) + { + float osc1_left_out[n_frames]; + float osc1_right_out[n_frames]; + float osc2_left_out[n_frames]; + float osc2_right_out[n_frames]; + + update_osc (voice->osc1_, osc_params[0]); + update_osc (voice->osc2_, osc_params[1]); + voice->osc1_.process_sample_stereo (osc1_left_out, osc1_right_out, n_frames); + voice->osc2_.process_sample_stereo (osc2_left_out, osc2_right_out, n_frames); + + // apply volume envelope & mix + const float mix_norm = get_param (pid_mix_) * 0.01; + const float v1 = voice->vel_gain_ * (1 - mix_norm); + const float v2 = voice->vel_gain_ * mix_norm; + for (uint i = 0; i < n_frames; i++) + { + mix_left_out[i] = osc1_left_out[i] * v1 + osc2_left_out[i] * v2; + mix_right_out[i] = osc1_right_out[i] * v1 + osc2_right_out[i] * v2; + } + /* --------- run ladder filter - processing in place is ok --------- */ + + /* TODO: under some conditions we could enable SSE in LadderVCF (alignment and block_size) */ + const float *inputs[2] = { mix_left_out, mix_right_out }; + float *outputs[2] = { mix_left_out, mix_right_out }; + double cutoff = convert_cutoff (get_param (pid_cutoff_)); + double key_track = get_param (pid_key_track_) * 0.01; + + if (fabs (voice->last_cutoff_ - cutoff) > 1e-7 || fabs (voice->last_key_track_ - key_track) > 1e-7) + { + const bool reset = voice->last_cutoff_ < -1000; + + // original strategy for key tracking: cutoff * exp (amount * log (key / 261.63)) + // but since cutoff_smooth_ is already in log2-frequency space, we can do it better + + voice->cutoff_smooth_.set (fast_log2 (cutoff) + key_track * fast_log2 (voice->freq_ / c3_hertz), reset); + voice->last_cutoff_ = cutoff; + voice->last_key_track_ = key_track; + } + double cut_mod = get_param (pid_fil_cut_mod_) / 12.; /* convert semitones to octaves */ + if (fabs (voice->last_cut_mod_ - cut_mod) > 1e-7) + { + const bool reset = voice->last_cut_mod_ < -1000; + + voice->cut_mod_smooth_.set (cut_mod, reset); + voice->last_cut_mod_ = cut_mod; + } + double resonance = get_param (pid_resonance_) * 0.01; + if (fabs (voice->last_reso_ - resonance) > 1e-7) + { + const bool reset = voice->last_reso_ < -1000; + + voice->reso_smooth_.set (resonance, reset); + voice->last_reso_ = resonance; + } + double drive = get_param (pid_drive_); + if (fabs (voice->last_drive_ - drive) > 1e-7) + { + const bool reset = voice->last_drive_ < -1000; + + voice->drive_smooth_.set (drive, reset); + voice->last_drive_ = drive; + } + /* TODO: possible improvements: + * - exponential smoothing (get rid of exp2f) + * - don't do anything if cutoff_smooth_->steps_ == 0 (add accessor) + */ + float freq_in[n_frames], reso_in[n_frames], drive_in[n_frames]; + for (uint i = 0; i < n_frames; i++) + { + if (INIT) + freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next()); + else + freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); + reso_in[i] = voice->reso_smooth_.get_next(); + drive_in[i] = voice->drive_smooth_.get_next(); + } + + int filter_type = irintf (get_param (pid_filter_type_)); + if (filter_type == 1) + { + voice->ladder_filter_.set_mode (LadderVCFMode (irintf (get_param (pid_ladder_mode_)))); + voice->ladder_filter_.set_drive (get_param (pid_drive_)); + voice->ladder_filter_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); + } + else if (filter_type == 2) + { + voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); + voice->skfilter_.process_block (n_frames, outputs[0], outputs[1], freq_in, reso_in, drive_in); + } + } void render (uint n_frames) override { @@ -638,96 +734,24 @@ class BlepSynth : public AudioProcessor { floatfill (left_out, 0.f, n_frames); floatfill (right_out, 0.f, n_frames); - for (auto& voice : active_voices_) + for (Voice *voice : active_voices_) { - float osc1_left_out[n_frames]; - float osc1_right_out[n_frames]; - float osc2_left_out[n_frames]; - float osc2_right_out[n_frames]; - - update_osc (voice->osc1_, osc_params[0]); - update_osc (voice->osc2_, osc_params[1]); - voice->osc1_.process_sample_stereo (osc1_left_out, osc1_right_out, n_frames); - voice->osc2_.process_sample_stereo (osc2_left_out, osc2_right_out, n_frames); - - // apply volume envelope & mix - float mix_left_out[n_frames]; - float mix_right_out[n_frames]; - const float mix_norm = get_param (pid_mix_) * 0.01; - const float v1 = voice->vel_gain_ * (1 - mix_norm); - const float v2 = voice->vel_gain_ * mix_norm; - for (uint i = 0; i < n_frames; i++) + if (voice->new_voice_) { - mix_left_out[i] = osc1_left_out[i] * v1 + osc2_left_out[i] * v2; - mix_right_out[i] = osc1_right_out[i] * v1 + osc2_right_out[i] * v2; - } - /* --------- run ladder filter - processing in place is ok --------- */ - - /* TODO: under some conditions we could enable SSE in LadderVCF (alignment and block_size) */ - const float *inputs[2] = { mix_left_out, mix_right_out }; - float *outputs[2] = { mix_left_out, mix_right_out }; - double cutoff = convert_cutoff (get_param (pid_cutoff_)); - double key_track = get_param (pid_key_track_) * 0.01; - - if (fabs (voice->last_cutoff_ - cutoff) > 1e-7 || fabs (voice->last_key_track_ - key_track) > 1e-7) - { - const bool reset = voice->last_cutoff_ < -1000; - - // original strategy for key tracking: cutoff * exp (amount * log (key / 261.63)) - // but since cutoff_smooth_ is already in log2-frequency space, we can do it better - - voice->cutoff_smooth_.set (fast_log2 (cutoff) + key_track * fast_log2 (voice->freq_ / c3_hertz), reset); - voice->last_cutoff_ = cutoff; - voice->last_key_track_ = key_track; - } - double cut_mod = get_param (pid_fil_cut_mod_) / 12.; /* convert semitones to octaves */ - if (fabs (voice->last_cut_mod_ - cut_mod) > 1e-7) - { - const bool reset = voice->last_cut_mod_ < -1000; - - voice->cut_mod_smooth_.set (cut_mod, reset); - voice->last_cut_mod_ = cut_mod; - } - double resonance = get_param (pid_resonance_) * 0.01; - if (fabs (voice->last_reso_ - resonance) > 1e-7) - { - const bool reset = voice->last_reso_ < -1000; - - voice->reso_smooth_.set (resonance, reset); - voice->last_reso_ = resonance; - } - double drive = get_param (pid_drive_); - if (fabs (voice->last_drive_ - drive) > 1e-7) - { - const bool reset = voice->last_drive_ < -1000; - - voice->drive_smooth_.set (drive, reset); - voice->last_drive_ = drive; - } - /* TODO: possible improvements: - * - exponential smoothing (get rid of exp2f) - * - don't do anything if cutoff_smooth_->steps_ == 0 (add accessor) - */ - float freq_in[n_frames], reso_in[n_frames], drive_in[n_frames]; - for (uint i = 0; i < n_frames; i++) - { - freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); - reso_in[i] = voice->reso_smooth_.get_next(); - drive_in[i] = voice->drive_smooth_.get_next(); + int filter_type = irintf (get_param (pid_filter_type_)); + if (filter_type == 2) + { + // compensate FIR oversampling filter latency + int idelay = voice->skfilter_.delay(); + float junk[idelay]; + render_voice (voice, voice->skfilter_.delay(), junk, junk); + } + voice->new_voice_ = false; } + float mix_left_out[n_frames]; + float mix_right_out[n_frames]; - int filter_type = irintf (get_param (pid_filter_type_)); - if (filter_type == 1) - { - voice->ladder_filter_.set_mode (LadderVCFMode (irintf (get_param (pid_ladder_mode_)))); - voice->ladder_filter_.set_drive (get_param (pid_drive_)); - voice->ladder_filter_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); - } - else if (filter_type == 2) - { - voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); - voice->skfilter_.process_block (n_frames, outputs[0], outputs[1], freq_in, reso_in, drive_in); - } + render_voice (voice, n_frames, mix_left_out, mix_right_out); // apply volume envelope float post_gain_factor = db2voltage (get_param (pid_post_gain_)); diff --git a/devices/blepsynth/skfilter.hh b/devices/blepsynth/skfilter.hh index a7eb4ecd..8a14c286 100644 --- a/devices/blepsynth/skfilter.hh +++ b/devices/blepsynth/skfilter.hh @@ -290,6 +290,11 @@ public: update_frequency_range(); } + double + delay() + { + return channels_[0].res_up->delay() / over_ + channels_[0].res_down->delay(); + } private: void update_frequency_range() From f234d86d9311facc1f5c1e33a5b03599e6d855a6 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Thu, 16 Feb 2023 14:08:37 +0100 Subject: [PATCH 09/17] DEVICES: blepsynth: update LadderVCF to version from dsp-research - oversample LadderVCF with factor 4, too Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 27 +-- devices/blepsynth/laddervcf.hh | 430 ++++++++++++++++----------------- 2 files changed, 221 insertions(+), 236 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 044210f8..88b72207 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -271,10 +271,10 @@ class BlepSynth : public AudioProcessor { BlepUtils::OscImpl osc1_; BlepUtils::OscImpl osc2_; - static constexpr int SKF_OVERSAMPLE = 4; + static constexpr int FILTER_OVERSAMPLE = 4; - LadderVCFNonLinear ladder_filter_; - SKFilter skfilter_ { SKF_OVERSAMPLE }; + LadderVCF ladder_filter_ { FILTER_OVERSAMPLE }; + SKFilter skfilter_ { FILTER_OVERSAMPLE }; }; std::vector voices_; std::vector active_voices_; @@ -602,7 +602,6 @@ class BlepSynth : public AudioProcessor { old_value = value; } } - template void render_voice (Voice *voice, uint n_frames, float *mix_left_out, float *mix_right_out) { @@ -625,11 +624,7 @@ class BlepSynth : public AudioProcessor { mix_left_out[i] = osc1_left_out[i] * v1 + osc2_left_out[i] * v2; mix_right_out[i] = osc1_right_out[i] * v1 + osc2_right_out[i] * v2; } - /* --------- run ladder filter - processing in place is ok --------- */ - - /* TODO: under some conditions we could enable SSE in LadderVCF (alignment and block_size) */ - const float *inputs[2] = { mix_left_out, mix_right_out }; - float *outputs[2] = { mix_left_out, mix_right_out }; + /* --------- run filter - processing in place is ok --------- */ double cutoff = convert_cutoff (get_param (pid_cutoff_)); double key_track = get_param (pid_key_track_) * 0.01; @@ -675,10 +670,7 @@ class BlepSynth : public AudioProcessor { float freq_in[n_frames], reso_in[n_frames], drive_in[n_frames]; for (uint i = 0; i < n_frames; i++) { - if (INIT) - freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next()); - else - freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); + freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); reso_in[i] = voice->reso_smooth_.get_next(); drive_in[i] = voice->drive_smooth_.get_next(); } @@ -687,13 +679,12 @@ class BlepSynth : public AudioProcessor { if (filter_type == 1) { voice->ladder_filter_.set_mode (LadderVCFMode (irintf (get_param (pid_ladder_mode_)))); - voice->ladder_filter_.set_drive (get_param (pid_drive_)); - voice->ladder_filter_.run_block (n_frames, cutoff, resonance, inputs, outputs, true, true, freq_in, nullptr, nullptr, nullptr); + voice->ladder_filter_.run_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); } else if (filter_type == 2) { voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); - voice->skfilter_.process_block (n_frames, outputs[0], outputs[1], freq_in, reso_in, drive_in); + voice->skfilter_.process_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); } } void @@ -744,14 +735,14 @@ class BlepSynth : public AudioProcessor { // compensate FIR oversampling filter latency int idelay = voice->skfilter_.delay(); float junk[idelay]; - render_voice (voice, voice->skfilter_.delay(), junk, junk); + render_voice (voice, voice->skfilter_.delay(), junk, junk); } voice->new_voice_ = false; } float mix_left_out[n_frames]; float mix_right_out[n_frames]; - render_voice (voice, n_frames, mix_left_out, mix_right_out); + render_voice (voice, n_frames, mix_left_out, mix_right_out); // apply volume envelope float post_gain_factor = db2voltage (get_param (pid_post_gain_)); diff --git a/devices/blepsynth/laddervcf.hh b/devices/blepsynth/laddervcf.hh index 77ff0345..a57e23b2 100644 --- a/devices/blepsynth/laddervcf.hh +++ b/devices/blepsynth/laddervcf.hh @@ -1,45 +1,56 @@ -// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +// Licensed GNU LGPL v2.1 or later: http://www.gnu.org/licenses/lgpl.html #ifndef __ASE_DEVICES_LADDER_VCF_HH__ #define __ASE_DEVICES_LADDER_VCF_HH__ -#include +#define PANDA_RESAMPLER_HEADER_ONLY +#include "pandaresampler.hh" + +#include namespace Ase { +using PandaResampler::Resampler2; + enum class LadderVCFMode { LP1, LP2, LP3, LP4 }; -template class LadderVCF { struct Channel { - double x1, x2, x3, x4; - double y1, y2, y3, y4; + float x1, x2, x3, x4; + float y1, y2, y3, y4; - // NOTE: Ase currently doesn't enforce SSE alignment so we force FPU resampling - Resampler2 res_up { Resampler2::UP, Resampler2::PREC_48DB, false }; - Resampler2 res_down { Resampler2::DOWN, Resampler2::PREC_48DB, false }; + std::unique_ptr res_up; + std::unique_ptr res_down; }; std::array channels; LadderVCFMode mode; - double pre_scale, post_scale; - double rate; - double freq_mod_octaves; - double key_tracking; - double resonance_mod; - - double last_key_tracking_factor; - float last_key_freq; - + float rate; + float freq_ = 440; + float reso_ = 0; + float drive_ = 0; + uint over_ = 0; + bool test_linear_ = false; + + struct FParams + { + float reso = 0; + float pre_scale = 1; + float post_scale = 1; + }; + FParams fparams_; + bool fparams_valid_ = false; public: - LadderVCF() + LadderVCF (int over) : + over_ (over) { + for (auto& channel : channels) + { + channel.res_up = std::make_unique (Resampler2::UP, over_, Resampler2::PREC_72DB); + channel.res_down = std::make_unique (Resampler2::DOWN, over_, Resampler2::PREC_72DB); + } reset(); set_mode (LadderVCFMode::LP4); - set_drive (0); set_rate (48000); - set_freq_mod_octaves (5); - set_key_tracking (0.5); - set_resonance_mod (0.1); } void set_mode (LadderVCFMode new_mode) @@ -47,36 +58,32 @@ public: mode = new_mode; } void - set_drive (double drive_db) + set_freq (float freq) { - const double drive_delta_db = 36; - - pre_scale = db2voltage (drive_db - drive_delta_db); - post_scale = std::max (1 / pre_scale, 1.0); + freq_ = freq; } void - set_rate (double r) + set_reso (float reso) { - rate = r; + reso_ = reso; + fparams_valid_ = false; } void - set_freq_mod_octaves (double octaves) + set_drive (float drive) { - freq_mod_octaves = octaves; + drive_ = drive; + fparams_valid_ = false; } void - set_key_tracking (double amount) + set_test_linear (bool test_linear) { - key_tracking = amount; - - // force recomputing key tracking factor - last_key_freq = -1; - last_key_tracking_factor = 0; + test_linear_ = test_linear; + fparams_valid_ = false; } void - set_resonance_mod (double amount) + set_rate (float r) { - resonance_mod = amount; + rate = r; } void reset() @@ -86,32 +93,45 @@ public: c.x1 = c.x2 = c.x3 = c.x4 = 0; c.y1 = c.y2 = c.y3 = c.y4 = 0; - c.res_up.reset(); - c.res_down.reset(); + c.res_up->reset(); + c.res_down->reset(); } - last_key_freq = -1; - last_key_tracking_factor = 0; + fparams_valid_ = false; + } + float + distort (float x) + { + /* shaped somewhat similar to tanh() and others, but faster */ + x = std::clamp (x, -1.0f, 1.0f); + + return x - x * x * x * (1.0f / 3); } - double - distort (double x) +private: + void + setup_reso_drive (FParams& fparams, float reso, float drive) { - if (NON_LINEAR) + if (test_linear_) // test filter as linear filter; don't do any resonance correction { - /* shaped somewhat similar to tanh() and others, but faster */ - x = std::clamp (x, -1.0, 1.0); + const float scale = 1e-5; + fparams.pre_scale = scale; + fparams.post_scale = 1 / scale; + fparams.reso = reso; - return x - x * x * x * (1.0 / 3); + return; } - else + const float db_x2_factor = 0.166096404744368; // 1/(20*log(2)/log(10)) + + // scale signal down (without normalization on output) for negative drive + float negative_drive_vol = 1; + if (drive < 0) { - return x; + negative_drive_vol = exp2f (drive * db_x2_factor); + drive = 0; } - } -private: - template inline bool - need_channel (uint ch) - { - return (1 << ch) & CHANNEL_MASK; + float vol = exp2f ((drive + -12 * reso) * db_x2_factor); + fparams.pre_scale = negative_drive_vol * vol; + fparams.post_scale = std::max (1 / vol, 1.0f); + fparams.reso = reso; } /* * This ladder filter implementation is mainly based on @@ -120,218 +140,192 @@ private: * Oscillator and Filter Algorithms for Virtual Analog Synthesis. * Computer Music Journal. 30. 19-31. 10.1162/comj.2006.30.2.19. */ - template inline void - run (double *values, double fc, double res) + template inline void + run (float *left, float *right, float fc) { - fc = M_PI * fc; - const double g = 0.9892 * fc - 0.4342 * fc * fc + 0.1381 * fc * fc * fc - 0.0202 * fc * fc * fc * fc; - const double gg = g * g; + const float pi = M_PI; + fc = pi * fc; + const float g = 0.9892f * fc - 0.4342f * fc * fc + 0.1381f * fc * fc * fc - 0.0202f * fc * fc * fc * fc; + const float b0 = g * (1 / 1.3f); + const float b1 = g * (0.3f / 1.3f); + const float a1 = g - 1; - res *= 1.0029 + 0.0526 * fc - 0.0926 * fc * fc + 0.0218 * fc * fc * fc; + float res = fparams_.reso; + res *= 1.0029f + 0.0526f * fc - 0.0926f * fc * fc + 0.0218f * fc * fc * fc; - constexpr uint oversample_count = OVERSAMPLE ? 2 : 1; - for (uint os = 0; os < oversample_count; os++) + for (uint os = 0; os < over_; os++) { - for (uint i = 0; i < channels.size(); i++) + for (uint i = 0; i < (STEREO ? 2 : 1); i++) { - if (need_channel (i)) + float &value = i == 0 ? left[os] : right[os]; + + Channel& c = channels[i]; + const float x = value * fparams_.pre_scale; + const float g_comp = 0.5f; // passband gain correction + const float x0 = distort (x - (c.y4 - g_comp * x) * res * 4); + + c.y1 = b0 * x0 + b1 * c.x1 - a1 * c.y1; + c.x1 = x0; + + c.y2 = b0 * c.y1 + b1 * c.x2 - a1 * c.y2; + c.x2 = c.y1; + + c.y3 = b0 * c.y2 + b1 * c.x3 - a1 * c.y3; + c.x3 = c.y2; + + c.y4 = b0 * c.y3 + b1 * c.x4 - a1 * c.y4; + c.x4 = c.y3; + + switch (MODE) { - Channel& c = channels[i]; - const double x = values[i] * pre_scale; - const double g_comp = 0.5; // passband gain correction - const double x0 = distort (x - (c.y4 - g_comp * x) * res * 4) * gg * gg * (1.0 / 1.3 / 1.3 / 1.3 / 1.3); - - c.y1 = x0 + c.x1 * 0.3 + c.y1 * (1 - g); - c.x1 = x0; - - c.y2 = c.y1 + c.x2 * 0.3 + c.y2 * (1 - g); - c.x2 = c.y1; - - c.y3 = c.y2 + c.x3 * 0.3 + c.y3 * (1 - g); - c.x3 = c.y2; - - c.y4 = c.y3 + c.x4 * 0.3 + c.y4 * (1 - g); - c.x4 = c.y3; - - switch (MODE) - { - case LadderVCFMode::LP1: - values[i] = c.y1 / (gg * g * (1.0 / (1.3 * 1.3 * 1.3))) * post_scale; - break; - case LadderVCFMode::LP2: - values[i] = c.y2 / (gg * (1.0 / (1.3 * 1.3))) * post_scale; - break; - case LadderVCFMode::LP3: - values[i] = c.y3 / (g * (1.0 / 1.3)) * post_scale; - break; - case LadderVCFMode::LP4: - values[i] = c.y4 * post_scale; - break; - default: - ASE_ASSERT_RETURN_UNREACHED(); - } + case LadderVCFMode::LP1: + value = c.y1 * fparams_.post_scale; + break; + case LadderVCFMode::LP2: + value = c.y2 * fparams_.post_scale; + break; + case LadderVCFMode::LP3: + value = c.y3 * fparams_.post_scale; + break; + case LadderVCFMode::LP4: + value = c.y4 * fparams_.post_scale; + break; + default: + assert (false); } } - values += channels.size(); } } - template inline void + template inline void do_run_block (uint n_samples, - double fc, - double res, - const float **inputs, - float **outputs, + float *left, + float *right, const float *freq_in, - const float *freq_mod_in, - const float *key_freq_in, - const float *reso_mod_in) + const float *reso_in, + const float *drive_in) { - float over_samples[2][2 * n_samples]; - float freq_scale = OVERSAMPLE ? 0.5 : 1.0; + float over_samples_left[over_ * n_samples]; + float over_samples_right[over_ * n_samples]; + float freq_scale = 1.0f / over_; float nyquist = rate * 0.5; - if (OVERSAMPLE) + channels[0].res_up->process_block (left, n_samples, over_samples_left); + if (STEREO) + channels[1].res_up->process_block (right, n_samples, over_samples_right); + + float fc = freq_ * freq_scale / nyquist; + + if (!fparams_valid_) { - for (size_t i = 0; i < channels.size(); i++) - if (need_channel (i)) - channels[i].res_up.process_block (inputs[i], n_samples, over_samples[i]); + setup_reso_drive (fparams_, reso_in ? reso_in[0] : reso_, drive_in ? drive_in[0] : drive_); + fparams_valid_ = true; } - fc *= freq_scale; - - for (uint i = 0; i < n_samples; i++) + if (reso_in || drive_in) { - double mod_fc = fc; - double mod_res = res; + /* for reso or drive modulation, we split the input it into small blocks + * and interpolate the pre_scale / post_scale / reso parameters + */ + float *left_blk = over_samples_left; + float *right_blk = over_samples_right; + + uint n_remaining_samples = n_samples; + while (n_remaining_samples) + { + const uint todo = std::min (n_remaining_samples, 64); - if (freq_in) - mod_fc = freq_in[i] * freq_scale / nyquist; + FParams fparams_end; + setup_reso_drive (fparams_end, reso_in ? reso_in[todo - 1] : reso_, drive_in ? drive_in[todo - 1] : drive_); - if (freq_mod_in) - mod_fc *= fast_exp2 (freq_mod_in[i] * freq_mod_octaves); + float todo_inv = 1.f / todo; + float delta_pre_scale = (fparams_end.pre_scale - fparams_.pre_scale) * todo_inv; + float delta_post_scale = (fparams_end.post_scale - fparams_.post_scale) * todo_inv; + float delta_reso = (fparams_end.reso - fparams_.reso) * todo_inv; - if (key_freq_in) - { - if (ASE_UNLIKELY (voltage_changed (key_freq_in[i], last_key_freq))) + uint j = 0; + for (uint i = 0; i < todo * over_; i += over_) { - /* key tracking from frequency is expensive to compute; with this optimization - * it should be fast, because the key frequency usually doesn't change - */ - last_key_freq = key_freq_in[i]; - last_key_tracking_factor = std::exp (key_tracking * std::log (voltage2hz (key_freq_in[i]) / c3_hertz)); + fparams_.pre_scale += delta_pre_scale; + fparams_.post_scale += delta_post_scale; + fparams_.reso += delta_reso; + + float mod_fc = fc; + + if (freq_in) + mod_fc = freq_in[j++] * freq_scale / nyquist; + + mod_fc = std::clamp (mod_fc, 0.0f, 1.0f); + + run (left_blk + i, right_blk + i, mod_fc); } - mod_fc *= last_key_tracking_factor; - } - if (reso_mod_in) - mod_res = std::clamp (mod_res + resonance_mod * reso_mod_in[i], 0.0, 1.0); - mod_fc = std::clamp (mod_fc, 0.0, 1.0); + n_remaining_samples -= todo; + left_blk += todo * over_; + right_blk += todo * over_; - if (OVERSAMPLE) - { - const uint over_pos = i * 2; - double values[4] = { - over_samples[0][over_pos], - over_samples[1][over_pos], - over_samples[0][over_pos + 1], - over_samples[1][over_pos + 1], - }; - - run (values, mod_fc, mod_res); - - over_samples[0][over_pos] = values[0]; - over_samples[1][over_pos] = values[1]; - over_samples[0][over_pos + 1] = values[2]; - over_samples[1][over_pos + 1] = values[3]; + if (freq_in) + freq_in += todo; + if (reso_in) + reso_in += todo; + if (drive_in) + drive_in += todo; } - else + } + else + { + for (uint i = 0; i < n_samples; i++) { - double values[2] = { inputs[0][i], inputs[1][i] }; + float mod_fc = fc; - run (values, mod_fc, mod_res); + if (freq_in) + mod_fc = freq_in[i] * freq_scale / nyquist; - outputs[0][i] = values[0]; - outputs[1][i] = values[1]; + mod_fc = std::clamp (mod_fc, 0.0f, 1.0f); + + const uint over_pos = i * over_; + run (over_samples_left + over_pos, over_samples_right + over_pos, mod_fc); } } - if (OVERSAMPLE) - { - for (size_t i = 0; i < channels.size(); i++) - if (need_channel (i)) - channels[i].res_down.process_block (over_samples[i], 2 * n_samples, outputs[i]); - } + channels[0].res_down->process_block (over_samples_left, over_ * n_samples, left); + if (STEREO) + channels[1].res_down->process_block (over_samples_right, over_ * n_samples, right); } template inline void run_block_mode (uint n_samples, - double fc, - double res, - const float **inputs, - float **outputs, - int channel_mask, + float *left, + float *right, const float *freq_in, - const float *freq_mod_in, - const float *key_freq_in, - const float *reso_mod_in) + const float *reso_in, + const float *drive_in) { - switch (channel_mask) - { - case 0: do_run_block (n_samples, fc, res, inputs, outputs, freq_in, freq_mod_in, key_freq_in, reso_mod_in); - break; - case 1: do_run_block (n_samples, fc, res, inputs, outputs, freq_in, freq_mod_in, key_freq_in, reso_mod_in); - break; - case 2: do_run_block (n_samples, fc, res, inputs, outputs, freq_in, freq_mod_in, key_freq_in, reso_mod_in); - break; - case 3: do_run_block (n_samples, fc, res, inputs, outputs, freq_in, freq_mod_in, key_freq_in, reso_mod_in); - break; - default: ASE_ASSERT_RETURN_UNREACHED(); - } + if (right) // stereo? + do_run_block (n_samples, left, right, freq_in, reso_in, drive_in); + else + do_run_block (n_samples, left, right, freq_in, reso_in, drive_in); } public: void - run_block (uint n_samples, - double fc, - double res, - const float **inputs, - float **outputs, - bool need_left, - bool need_right, - const float *freq_in, - const float *freq_mod_in, - const float *key_freq_in, - const float *reso_mod_in) + run_block (uint n_samples, + float *left, + float *right, + const float *freq_in = nullptr, + const float *reso_in = nullptr, + const float *drive_in = nullptr) { - int channel_mask = 0; - if (need_left) - channel_mask |= 1; - if (need_right) - channel_mask |= 2; switch (mode) { - case LadderVCFMode::LP4: run_block_mode (n_samples, fc, res, inputs, outputs, channel_mask, - freq_in, freq_mod_in, key_freq_in, reso_mod_in); + case LadderVCFMode::LP4: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); break; - case LadderVCFMode::LP3: run_block_mode (n_samples, fc, res, inputs, outputs, channel_mask, - freq_in, freq_mod_in, key_freq_in, reso_mod_in); + case LadderVCFMode::LP3: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); break; - case LadderVCFMode::LP2: run_block_mode (n_samples, fc, res, inputs, outputs, channel_mask, - freq_in, freq_mod_in, key_freq_in, reso_mod_in); + case LadderVCFMode::LP2: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); break; - case LadderVCFMode::LP1: run_block_mode (n_samples, fc, res, inputs, outputs, channel_mask, - freq_in, freq_mod_in, key_freq_in, reso_mod_in); + case LadderVCFMode::LP1: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); break; } } }; -// fast linear model of the filter -typedef LadderVCF LadderVCFLinear; - -// slow but accurate non-linear model of the filter (uses oversampling) -typedef LadderVCF LadderVCFNonLinear; - -// fast non-linear version (no oversampling), may have aliasing -typedef LadderVCF LadderVCFNonLinearCheap; - } // Ase #endif // __ASE_DEVICES_LADDER_VCF_HH__ From 311d4c91906e59b359807e91bdf245842250cd23 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Thu, 16 Feb 2023 14:43:32 +0100 Subject: [PATCH 10/17] DEVICES: blepsynth: add latency compensation for LadderVCF Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 18 ++++++++++++------ devices/blepsynth/laddervcf.hh | 10 ++++++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 88b72207..75b6e4b1 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -224,6 +224,8 @@ class BlepSynth : public AudioProcessor { ParamId pid_ladder_mode_; ParamId pid_skfilter_mode_; + enum { FILTER_TYPE_BYPASS, FILTER_TYPE_LADDER, FILTER_TYPE_SKFILTER }; + ParamId pid_attack_; ParamId pid_decay_; ParamId pid_sustain_; @@ -314,7 +316,7 @@ class BlepSynth : public AudioProcessor { filter_type_choices += { "—"_uc, "Bypass Filter" }; filter_type_choices += { "LD"_uc, "Ladder Filter" }; filter_type_choices += { "SKF"_uc, "Sallen-Key Filter" }; - pid_filter_type_ = add_param ("Filter Type", "Type", std::move (filter_type_choices), 1, "", "Filter Type to be used"); + pid_filter_type_ = add_param ("Filter Type", "Type", std::move (filter_type_choices), FILTER_TYPE_LADDER, "", "Filter Type to be used"); ChoiceS ladder_mode_choices; ladder_mode_choices += { "LP1"_uc, "1 Pole Lowpass, 6dB/Octave" }; @@ -676,12 +678,12 @@ class BlepSynth : public AudioProcessor { } int filter_type = irintf (get_param (pid_filter_type_)); - if (filter_type == 1) + if (filter_type == FILTER_TYPE_LADDER) { voice->ladder_filter_.set_mode (LadderVCFMode (irintf (get_param (pid_ladder_mode_)))); voice->ladder_filter_.run_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); } - else if (filter_type == 2) + else if (filter_type == FILTER_TYPE_SKFILTER) { voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); voice->skfilter_.process_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); @@ -730,12 +732,16 @@ class BlepSynth : public AudioProcessor { if (voice->new_voice_) { int filter_type = irintf (get_param (pid_filter_type_)); - if (filter_type == 2) + int idelay = 0; + if (filter_type == FILTER_TYPE_LADDER) + idelay = voice->ladder_filter_.delay(); + if (filter_type == FILTER_TYPE_SKFILTER) + idelay = voice->skfilter_.delay(); + if (idelay) { // compensate FIR oversampling filter latency - int idelay = voice->skfilter_.delay(); float junk[idelay]; - render_voice (voice, voice->skfilter_.delay(), junk, junk); + render_voice (voice, idelay, junk, junk); } voice->new_voice_ = false; } diff --git a/devices/blepsynth/laddervcf.hh b/devices/blepsynth/laddervcf.hh index a57e23b2..daa19e06 100644 --- a/devices/blepsynth/laddervcf.hh +++ b/devices/blepsynth/laddervcf.hh @@ -1,4 +1,5 @@ -// Licensed GNU LGPL v2.1 or later: http://www.gnu.org/licenses/lgpl.html +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 + #ifndef __ASE_DEVICES_LADDER_VCF_HH__ #define __ASE_DEVICES_LADDER_VCF_HH__ @@ -98,6 +99,12 @@ public: } fparams_valid_ = false; } + double + delay() + { + return channels[0].res_up->delay() / over_ + channels[0].res_down->delay(); + } +private: float distort (float x) { @@ -106,7 +113,6 @@ public: return x - x * x * x * (1.0f / 3); } -private: void setup_reso_drive (FParams& fparams, float reso, float drive) { From ebf2a9157ef3d938bedd435ea2959ae53e63879a Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Thu, 16 Feb 2023 17:29:31 +0100 Subject: [PATCH 11/17] DEVICES: blepsynth: update LadderVCF, setup cutoff frequency range Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 15 ++- devices/blepsynth/laddervcf.hh | 192 +++++++++++++++++++-------------- 2 files changed, 125 insertions(+), 82 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index 75b6e4b1..cf6d0e12 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -226,6 +226,9 @@ class BlepSynth : public AudioProcessor { enum { FILTER_TYPE_BYPASS, FILTER_TYPE_LADDER, FILTER_TYPE_SKFILTER }; + static constexpr int CUTOFF_MIN_MIDI = 15; + static constexpr int CUTOFF_MAX_MIDI = 144; + ParamId pid_attack_; ParamId pid_decay_; ParamId pid_sustain_; @@ -308,7 +311,7 @@ class BlepSynth : public AudioProcessor { start_group ("Filter"); - pid_cutoff_ = add_param ("Cutoff", "Cutoff", 15, 144, 60); // cutoff as midi notes + pid_cutoff_ = add_param ("Cutoff", "Cutoff", CUTOFF_MIN_MIDI, CUTOFF_MAX_MIDI, 60); // cutoff as midi notes pid_resonance_ = add_param ("Resonance", "Reso", 0, 100, 25.0, "%"); pid_drive_ = add_param ("Drive", "Drive", -24, 36, 0, "dB"); pid_key_track_ = add_param ("Key Tracking", "KeyTr", 0, 100, 50, "%"); @@ -555,12 +558,16 @@ class BlepSynth : public AudioProcessor { voice->osc1_.reset(); voice->osc2_.reset(); + const float cutoff_min_hz = convert_cutoff (CUTOFF_MIN_MIDI); + const float cutoff_max_hz = convert_cutoff (CUTOFF_MAX_MIDI); + voice->ladder_filter_.reset(); voice->ladder_filter_.set_rate (sample_rate()); + voice->ladder_filter_.set_frequency_range (cutoff_min_hz, cutoff_max_hz); voice->skfilter_.reset(); voice->skfilter_.set_rate (sample_rate()); - voice->skfilter_.set_frequency_range (10, 30000); + voice->skfilter_.set_frequency_range (cutoff_min_hz, cutoff_max_hz); voice->new_voice_ = true; voice->cutoff_smooth_.reset (sample_rate(), 0.020); @@ -680,8 +687,8 @@ class BlepSynth : public AudioProcessor { int filter_type = irintf (get_param (pid_filter_type_)); if (filter_type == FILTER_TYPE_LADDER) { - voice->ladder_filter_.set_mode (LadderVCFMode (irintf (get_param (pid_ladder_mode_)))); - voice->ladder_filter_.run_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); + voice->ladder_filter_.set_mode (LadderVCF::Mode (irintf (get_param (pid_ladder_mode_)))); + voice->ladder_filter_.process_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); } else if (filter_type == FILTER_TYPE_SKFILTER) { diff --git a/devices/blepsynth/laddervcf.hh b/devices/blepsynth/laddervcf.hh index daa19e06..4dc30d8b 100644 --- a/devices/blepsynth/laddervcf.hh +++ b/devices/blepsynth/laddervcf.hh @@ -12,10 +12,13 @@ namespace Ase { using PandaResampler::Resampler2; -enum class LadderVCFMode { LP1, LP2, LP3, LP4 }; - class LadderVCF { +public: + enum Mode { + LP1, LP2, LP3, LP4 + }; +private: struct Channel { float x1, x2, x3, x4; float y1, y2, y3, y4; @@ -23,15 +26,22 @@ class LadderVCF std::unique_ptr res_up; std::unique_ptr res_down; }; - std::array channels; - LadderVCFMode mode; - float rate; + std::array channels_; + Mode mode_; + float rate_ = 0; + float freq_scale_factor_ = 0; + float frequency_range_min_ = 0; + float frequency_range_max_ = 0; + float clamp_freq_min_ = 0; + float clamp_freq_max_ = 0; float freq_ = 440; float reso_ = 0; float drive_ = 0; uint over_ = 0; bool test_linear_ = false; + static constexpr uint MAX_BLOCK_SIZE = 1024; + struct FParams { float reso = 0; @@ -44,19 +54,20 @@ public: LadderVCF (int over) : over_ (over) { - for (auto& channel : channels) + for (auto& channel : channels_) { channel.res_up = std::make_unique (Resampler2::UP, over_, Resampler2::PREC_72DB); channel.res_down = std::make_unique (Resampler2::DOWN, over_, Resampler2::PREC_72DB); } - reset(); - set_mode (LadderVCFMode::LP4); + set_mode (Mode::LP4); set_rate (48000); + set_frequency_range (10, 24000); + reset(); } void - set_mode (LadderVCFMode new_mode) + set_mode (Mode new_mode) { - mode = new_mode; + mode_ = new_mode; } void set_freq (float freq) @@ -84,12 +95,23 @@ public: void set_rate (float r) { - rate = r; + rate_ = r; + freq_scale_factor_ = 2 * M_PI / (rate_ * over_); + + update_frequency_range(); + } + void + set_frequency_range (float min_freq, float max_freq) + { + frequency_range_min_ = min_freq; + frequency_range_max_ = max_freq; + + update_frequency_range(); } void reset() { - for (auto& c : channels) + for (auto& c : channels_) { c.x1 = c.x2 = c.x3 = c.x4 = 0; c.y1 = c.y2 = c.y3 = c.y4 = 0; @@ -102,9 +124,18 @@ public: double delay() { - return channels[0].res_up->delay() / over_ + channels[0].res_down->delay(); + return channels_[0].res_up->delay() / over_ + channels_[0].res_down->delay(); } private: + void + update_frequency_range() + { + /* we want to clamp to the user defined range (set_frequency_range()) + * but also enforce that the filter is well below nyquist frequency + */ + clamp_freq_min_ = frequency_range_min_; + clamp_freq_max_ = std::min (frequency_range_max_, rate_ * over_ * 0.49f); + } float distort (float x) { @@ -146,11 +177,10 @@ private: * Oscillator and Filter Algorithms for Virtual Analog Synthesis. * Computer Music Journal. 30. 19-31. 10.1162/comj.2006.30.2.19. */ - template inline void - run (float *left, float *right, float fc) + template inline void + run (float *left, float *right, float freq) { - const float pi = M_PI; - fc = pi * fc; + const float fc = std::clamp (freq, clamp_freq_min_, clamp_freq_max_) * freq_scale_factor_; const float g = 0.9892f * fc - 0.4342f * fc * fc + 0.1381f * fc * fc * fc - 0.0202f * fc * fc * fc * fc; const float b0 = g * (1 / 1.3f); const float b1 = g * (0.3f / 1.3f); @@ -165,7 +195,7 @@ private: { float &value = i == 0 ? left[os] : right[os]; - Channel& c = channels[i]; + Channel& c = channels_[i]; const float x = value * fparams_.pre_scale; const float g_comp = 0.5f; // passband gain correction const float x0 = distort (x - (c.y4 - g_comp * x) * res * 4); @@ -184,16 +214,16 @@ private: switch (MODE) { - case LadderVCFMode::LP1: + case LP1: value = c.y1 * fparams_.post_scale; break; - case LadderVCFMode::LP2: + case LP2: value = c.y2 * fparams_.post_scale; break; - case LadderVCFMode::LP3: + case LP3: value = c.y3 * fparams_.post_scale; break; - case LadderVCFMode::LP4: + case LP4: value = c.y4 * fparams_.post_scale; break; default: @@ -202,24 +232,20 @@ private: } } } - template inline void - do_run_block (uint n_samples, - float *left, - float *right, - const float *freq_in, - const float *reso_in, - const float *drive_in) + template inline void + do_process_block (uint n_samples, + float *left, + float *right, + const float *freq_in, + const float *reso_in, + const float *drive_in) { float over_samples_left[over_ * n_samples]; float over_samples_right[over_ * n_samples]; - float freq_scale = 1.0f / over_; - float nyquist = rate * 0.5; - channels[0].res_up->process_block (left, n_samples, over_samples_left); + channels_[0].res_up->process_block (left, n_samples, over_samples_left); if (STEREO) - channels[1].res_up->process_block (right, n_samples, over_samples_right); - - float fc = freq_ * freq_scale / nyquist; + channels_[1].res_up->process_block (right, n_samples, over_samples_right); if (!fparams_valid_) { @@ -255,14 +281,9 @@ private: fparams_.post_scale += delta_post_scale; fparams_.reso += delta_reso; - float mod_fc = fc; - - if (freq_in) - mod_fc = freq_in[j++] * freq_scale / nyquist; - - mod_fc = std::clamp (mod_fc, 0.0f, 1.0f); + float freq = freq_in ? freq_in[j++] : freq_; - run (left_blk + i, right_blk + i, mod_fc); + run (left_blk + i, right_blk + i, freq); } n_remaining_samples -= todo; @@ -279,56 +300,71 @@ private: } else { + uint over_pos = 0; + for (uint i = 0; i < n_samples; i++) { - float mod_fc = fc; - - if (freq_in) - mod_fc = freq_in[i] * freq_scale / nyquist; + float freq = freq_in ? freq_in[i] : freq_; - mod_fc = std::clamp (mod_fc, 0.0f, 1.0f); - - const uint over_pos = i * over_; - run (over_samples_left + over_pos, over_samples_right + over_pos, mod_fc); + run (over_samples_left + over_pos, over_samples_right + over_pos, freq); + over_pos += over_; } } - channels[0].res_down->process_block (over_samples_left, over_ * n_samples, left); + channels_[0].res_down->process_block (over_samples_left, over_ * n_samples, left); if (STEREO) - channels[1].res_down->process_block (over_samples_right, over_ * n_samples, right); + channels_[1].res_down->process_block (over_samples_right, over_ * n_samples, right); } - template inline void - run_block_mode (uint n_samples, - float *left, - float *right, - const float *freq_in, - const float *reso_in, - const float *drive_in) + template inline void + process_block_mode (uint n_samples, + float *left, + float *right, + const float *freq_in, + const float *reso_in, + const float *drive_in) { if (right) // stereo? - do_run_block (n_samples, left, right, freq_in, reso_in, drive_in); + do_process_block (n_samples, left, right, freq_in, reso_in, drive_in); else - do_run_block (n_samples, left, right, freq_in, reso_in, drive_in); + do_process_block (n_samples, left, right, freq_in, reso_in, drive_in); } public: void - run_block (uint n_samples, - float *left, - float *right, - const float *freq_in = nullptr, - const float *reso_in = nullptr, - const float *drive_in = nullptr) + process_block (uint n_samples, + float *left, + float *right, + const float *freq_in = nullptr, + const float *reso_in = nullptr, + const float *drive_in = nullptr) { - switch (mode) - { - case LadderVCFMode::LP4: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); - break; - case LadderVCFMode::LP3: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); - break; - case LadderVCFMode::LP2: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); - break; - case LadderVCFMode::LP1: run_block_mode (n_samples, left, right, freq_in, reso_in, drive_in); - break; - } + while (n_samples) + { + const uint todo = std::min (n_samples, MAX_BLOCK_SIZE); + + switch (mode_) + { + case LP4: process_block_mode (todo, left, right, freq_in, reso_in, drive_in); + break; + case LP3: process_block_mode (todo, left, right, freq_in, reso_in, drive_in); + break; + case LP2: process_block_mode (todo, left, right, freq_in, reso_in, drive_in); + break; + case LP1: process_block_mode (todo, left, right, freq_in, reso_in, drive_in); + break; + } + + if (left) + left += todo; + if (right) + right += todo; + if (freq_in) + freq_in += todo; + if (reso_in) + reso_in += todo; + if (drive_in) + drive_in += todo; + + n_samples -= todo; + } } }; From 491952fee745ebe94e6eac2c4499afbf08daa574 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Fri, 17 Feb 2023 12:45:21 +0100 Subject: [PATCH 12/17] DEVICES: blepsynth: optimize filter code if freq/reso/drive are constant Signed-off-by: Stefan Westerfeld --- devices/blepsynth/blepsynth.cc | 56 ++++++++++++++++++++++++------- devices/blepsynth/linearsmooth.hh | 5 +++ devices/blepsynth/skfilter.hh | 2 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index cf6d0e12..c0c7f5ea 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -149,7 +149,11 @@ class Envelope params_.delta = (end_x - RATIO * (start_x - end_x)) * (1 - params_.factor); } } - + bool + is_constant() + { + return state_ == State::SUSTAIN || state_ == State::DONE; + } float get_next() { @@ -672,28 +676,54 @@ class BlepSynth : public AudioProcessor { voice->drive_smooth_.set (drive, reset); voice->last_drive_ = drive; } - /* TODO: possible improvements: - * - exponential smoothing (get rid of exp2f) - * - don't do anything if cutoff_smooth_->steps_ == 0 (add accessor) - */ - float freq_in[n_frames], reso_in[n_frames], drive_in[n_frames]; - for (uint i = 0; i < n_frames; i++) + + auto filter_process_block = [&] (auto& filter) { - freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); - reso_in[i] = voice->reso_smooth_.get_next(); - drive_in[i] = voice->drive_smooth_.get_next(); - } + auto gen_filter_input = [&] (float *freq_in, float *reso_in, float *drive_in, uint n_frames) + { + for (uint i = 0; i < n_frames; i++) + { + freq_in[i] = fast_exp2 (voice->cutoff_smooth_.get_next() + voice->fil_envelope_.get_next() * voice->cut_mod_smooth_.get_next()); + reso_in[i] = voice->reso_smooth_.get_next(); + drive_in[i] = voice->drive_smooth_.get_next(); + } + }; + + bool const_freq = voice->cutoff_smooth_.is_constant() && voice->fil_envelope_.is_constant() && voice->cut_mod_smooth_.is_constant(); + bool const_reso = voice->reso_smooth_.is_constant(); + bool const_drive = voice->drive_smooth_.is_constant(); + + if (const_freq && const_reso && const_drive) + { + /* use more efficient version of the filter computation if all parameters are constants */ + float freq, reso, drive; + gen_filter_input (&freq, &reso, &drive, 1); + + filter.set_freq (freq); + filter.set_reso (reso); + filter.set_drive (drive); + filter.process_block (n_frames, mix_left_out, mix_right_out); + } + else + { + /* generic version: pass per-sample values for freq, reso and drive */ + float freq_in[n_frames], reso_in[n_frames], drive_in[n_frames]; + gen_filter_input (freq_in, reso_in, drive_in, n_frames); + + filter.process_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); + } + }; int filter_type = irintf (get_param (pid_filter_type_)); if (filter_type == FILTER_TYPE_LADDER) { voice->ladder_filter_.set_mode (LadderVCF::Mode (irintf (get_param (pid_ladder_mode_)))); - voice->ladder_filter_.process_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); + filter_process_block (voice->ladder_filter_); } else if (filter_type == FILTER_TYPE_SKFILTER) { voice->skfilter_.set_mode (SKFilter::Mode (irintf (get_param (pid_skfilter_mode_)))); - voice->skfilter_.process_block (n_frames, mix_left_out, mix_right_out, freq_in, reso_in, drive_in); + filter_process_block (voice->skfilter_); } } void diff --git a/devices/blepsynth/linearsmooth.hh b/devices/blepsynth/linearsmooth.hh index 147e9179..9eaa633b 100644 --- a/devices/blepsynth/linearsmooth.hh +++ b/devices/blepsynth/linearsmooth.hh @@ -53,6 +53,11 @@ public: return linear_value_; } } + bool + is_constant() + { + return steps_ == 0; + } }; } diff --git a/devices/blepsynth/skfilter.hh b/devices/blepsynth/skfilter.hh index 8a14c286..662661c3 100644 --- a/devices/blepsynth/skfilter.hh +++ b/devices/blepsynth/skfilter.hh @@ -564,7 +564,7 @@ private: } public: void - process_block (uint n_samples, float *left, float *right, const float *freq_in, const float *reso_in = nullptr, const float *drive_in = nullptr) + process_block (uint n_samples, float *left, float *right, const float *freq_in = nullptr, const float *reso_in = nullptr, const float *drive_in = nullptr) { static constexpr auto jump_table { make_jump_table (std::make_index_sequence()) }; From abbae1c64fe335c2e4d005b41e918f22fcc632f9 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Fri, 17 Feb 2023 17:28:32 +0100 Subject: [PATCH 13/17] DEVICES: blepsynth: update LadderVCF with drive/reso boost Signed-off-by: Stefan Westerfeld --- devices/blepsynth/laddervcf.hh | 39 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/devices/blepsynth/laddervcf.hh b/devices/blepsynth/laddervcf.hh index 4dc30d8b..daf38e10 100644 --- a/devices/blepsynth/laddervcf.hh +++ b/devices/blepsynth/laddervcf.hh @@ -1,7 +1,6 @@ // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 -#ifndef __ASE_DEVICES_LADDER_VCF_HH__ -#define __ASE_DEVICES_LADDER_VCF_HH__ +#pragma once #define PANDA_RESAMPLER_HEADER_ONLY #include "pandaresampler.hh" @@ -147,12 +146,14 @@ private: void setup_reso_drive (FParams& fparams, float reso, float drive) { + reso = std::clamp (reso, 0.001f, 1.f); + if (test_linear_) // test filter as linear filter; don't do any resonance correction { const float scale = 1e-5; fparams.pre_scale = scale; fparams.post_scale = 1 / scale; - fparams.reso = reso; + fparams.reso = reso * 4; return; } @@ -165,10 +166,14 @@ private: negative_drive_vol = exp2f (drive * db_x2_factor); drive = 0; } - float vol = exp2f ((drive + -12 * reso) * db_x2_factor); + // drive resonance boost + if (drive > 0) + reso += drive * sqrt (reso) * reso * 0.03f; + + float vol = exp2f ((drive + -12 * sqrt (reso)) * db_x2_factor); fparams.pre_scale = negative_drive_vol * vol; fparams.post_scale = std::max (1 / vol, 1.0f); - fparams.reso = reso; + fparams.reso = sqrt (reso) * 4; } /* * This ladder filter implementation is mainly based on @@ -178,7 +183,7 @@ private: * Computer Music Journal. 30. 19-31. 10.1162/comj.2006.30.2.19. */ template inline void - run (float *left, float *right, float freq) + run (float *left, float *right, float freq, uint n_samples) { const float fc = std::clamp (freq, clamp_freq_min_, clamp_freq_max_) * freq_scale_factor_; const float g = 0.9892f * fc - 0.4342f * fc * fc + 0.1381f * fc * fc * fc - 0.0202f * fc * fc * fc * fc; @@ -189,7 +194,7 @@ private: float res = fparams_.reso; res *= 1.0029f + 0.0526f * fc - 0.0926f * fc * fc + 0.0218f * fc * fc * fc; - for (uint os = 0; os < over_; os++) + for (uint os = 0; os < n_samples; os++) { for (uint i = 0; i < (STEREO ? 2 : 1); i++) { @@ -198,7 +203,7 @@ private: Channel& c = channels_[i]; const float x = value * fparams_.pre_scale; const float g_comp = 0.5f; // passband gain correction - const float x0 = distort (x - (c.y4 - g_comp * x) * res * 4); + const float x0 = distort (x - (c.y4 - g_comp * x) * res); c.y1 = b0 * x0 + b1 * c.x1 - a1 * c.y1; c.x1 = x0; @@ -283,7 +288,7 @@ private: float freq = freq_in ? freq_in[j++] : freq_; - run (left_blk + i, right_blk + i, freq); + run (left_blk + i, right_blk + i, freq, over_); } n_remaining_samples -= todo; @@ -298,18 +303,20 @@ private: drive_in += todo; } } - else + else if (freq_in) { uint over_pos = 0; for (uint i = 0; i < n_samples; i++) { - float freq = freq_in ? freq_in[i] : freq_; - - run (over_samples_left + over_pos, over_samples_right + over_pos, freq); + run (over_samples_left + over_pos, over_samples_right + over_pos, freq_in[i], over_); over_pos += over_; } } + else + { + run (over_samples_left, over_samples_right, freq_, n_samples * over_); + } channels_[0].res_down->process_block (over_samples_left, over_ * n_samples, left); if (STEREO) channels_[1].res_down->process_block (over_samples_right, over_ * n_samples, right); @@ -331,7 +338,7 @@ public: void process_block (uint n_samples, float *left, - float *right, + float *right = nullptr, const float *freq_in = nullptr, const float *reso_in = nullptr, const float *drive_in = nullptr) @@ -368,6 +375,4 @@ public: } }; -} // Ase - -#endif // __ASE_DEVICES_LADDER_VCF_HH__ +} // SpectMorph From 8c55f508cbceabb6447f923a9941b7b488bed7fc Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Mon, 16 Oct 2023 12:48:58 +0200 Subject: [PATCH 14/17] EXTERNAL: pandaresampler: add swesterfeld/pandaresampler commit from 2023-02-14 10:18:45 git submodule add --name pandaresampler https://github.com/swesterfeld/pandaresampler.git external/pandaresampler git -C external/pandaresampler checkout 29097dc786b6d75a9deb12e70ec8960a78f7f8f5 Signed-off-by: Tim Janik --- .gitmodules | 3 +++ external/pandaresampler | 1 + 2 files changed, 4 insertions(+) create mode 160000 external/pandaresampler diff --git a/.gitmodules b/.gitmodules index 4b873336..3fdbb69c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "blobs4anklang"] path = external/blobs4anklang url = https://github.com/tim-janik/blobs4anklang.git +[submodule "pandaresampler"] + path = external/pandaresampler + url = https://github.com/swesterfeld/pandaresampler.git diff --git a/external/pandaresampler b/external/pandaresampler new file mode 160000 index 00000000..29097dc7 --- /dev/null +++ b/external/pandaresampler @@ -0,0 +1 @@ +Subproject commit 29097dc786b6d75a9deb12e70ec8960a78f7f8f5 From ae7361e3ffcb8b71b0a0df9a611fdfb89a159d79 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Tue, 17 Oct 2023 12:43:33 +0200 Subject: [PATCH 15/17] DEVICES: blepsynth/Makefile.mk: include external/pandaresampler/lib Signed-off-by: Tim Janik --- devices/blepsynth/Makefile.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devices/blepsynth/Makefile.mk b/devices/blepsynth/Makefile.mk index ca233a71..290c5dd5 100644 --- a/devices/blepsynth/Makefile.mk +++ b/devices/blepsynth/Makefile.mk @@ -4,4 +4,4 @@ devices/4ase.ccfiles += $(strip \ devices/blepsynth/bleposcdata.cc \ devices/blepsynth/blepsynth.cc \ ) -$>/devices/blepsynth/blepsynth.o: EXTRA_FLAGS ::= -I/home/stefan/src/pandaresampler/lib +$>/devices/blepsynth/blepsynth.o: EXTRA_FLAGS ::= -Iexternal/pandaresampler/lib From 90bf331191ba81e68a91bd4f6d6b533e632973d8 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Wed, 18 Oct 2023 13:48:50 +0200 Subject: [PATCH 16/17] DEVICES: blepsynth/blepsynth.cc: reorder parameter groups Signed-off-by: Tim Janik --- devices/blepsynth/blepsynth.cc | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/devices/blepsynth/blepsynth.cc b/devices/blepsynth/blepsynth.cc index c0c7f5ea..f786db9d 100644 --- a/devices/blepsynth/blepsynth.cc +++ b/devices/blepsynth/blepsynth.cc @@ -313,6 +313,20 @@ class BlepSynth : public AudioProcessor { oscparams (0); + start_group ("Mix"); + pid_mix_ = add_param ("Mix", "Mix", 0, 100, 0, "%"); + pid_vel_track_ = add_param ("Velocity Tracking", "VelTr", 0, 100, 50, "%"); + // TODO: this probably should default to 0dB once we have track/mixer volumes + pid_post_gain_ = add_param ("Post Gain", "Gain", -24, 24, -12, "dB"); + + oscparams (1); + + start_group ("Volume Envelope"); + pid_attack_ = add_param ("Attack", "A", 0, 100, 20.0, "%"); + pid_decay_ = add_param ("Decay", "D", 0, 100, 30.0, "%"); + pid_sustain_ = add_param ("Sustain", "S", 0, 100, 50.0, "%"); + pid_release_ = add_param ("Release", "R", 0, 100, 30.0, "%"); + start_group ("Filter"); pid_cutoff_ = add_param ("Cutoff", "Cutoff", CUTOFF_MIN_MIDI, CUTOFF_MAX_MIDI, 60); // cutoff as midi notes @@ -351,14 +365,6 @@ class BlepSynth : public AudioProcessor { skfilter_mode_choices += { "HP8"_uc, "8 Pole Highpass, 48dB/Octave" }; pid_skfilter_mode_ = add_param ("SKFilter Mode", "Mode", std::move (skfilter_mode_choices), 2, "", "Sallen-Key Filter Mode to be used"); - oscparams (1); - - start_group ("Volume Envelope"); - pid_attack_ = add_param ("Attack", "A", 0, 100, 20.0, "%"); - pid_decay_ = add_param ("Decay", "D", 0, 100, 30.0, "%"); - pid_sustain_ = add_param ("Sustain", "S", 0, 100, 50.0, "%"); - pid_release_ = add_param ("Release", "R", 0, 100, 30.0, "%"); - start_group ("Filter Envelope"); pid_fil_attack_ = add_param ("Attack", "A", 0, 100, 40, "%"); pid_fil_decay_ = add_param ("Decay", "D", 0, 100, 55, "%"); @@ -366,12 +372,6 @@ class BlepSynth : public AudioProcessor { pid_fil_release_ = add_param ("Release", "R", 0, 100, 30, "%"); pid_fil_cut_mod_ = add_param ("Env Cutoff Modulation", "CutMod", -96, 96, 36, "semitones"); /* 8 octaves range */ - start_group ("Mix"); - pid_mix_ = add_param ("Mix", "Mix", 0, 100, 0, "%"); - pid_vel_track_ = add_param ("Velocity Tracking", "VelTr", 0, 100, 50, "%"); - // TODO: this probably should default to 0dB once we have track/mixer volumes - pid_post_gain_ = add_param ("Post Gain", "Gain", -24, 24, -12, "dB"); - start_group ("Keyboard Input"); pid_c_ = add_param ("Main Input 1", "C", false, GUIONLY); pid_d_ = add_param ("Main Input 2", "D", false, GUIONLY); From 23eaa68abbc4a65da0b1366e86d1cd28516382d3 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 28 Oct 2023 21:21:06 +0200 Subject: [PATCH 17/17] ASE: resampler2: remove, superseded by pandaresampler Signed-off-by: Tim Janik --- ase/resampler2.cc | 890 ---------------------------------------------- ase/resampler2.hh | 148 -------- 2 files changed, 1038 deletions(-) delete mode 100644 ase/resampler2.cc delete mode 100644 ase/resampler2.hh diff --git a/ase/resampler2.cc b/ase/resampler2.cc deleted file mode 100644 index 88430bfe..00000000 --- a/ase/resampler2.cc +++ /dev/null @@ -1,890 +0,0 @@ -// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 -#include "resampler2.hh" -#include "datautils.hh" -#include "memory.hh" -#include "internal.hh" -#ifdef __SSE__ -#include -#endif - -namespace Ase { - -/* see: http://ds9a.nl/gcc-simd/ */ -union F4Vector -{ - float f[4]; -#ifdef __SSE__ - __m128 v; // vector of four single floats -#endif -}; - -using std::min; -using std::max; -using std::copy; - -/* --- Resampler2 methods --- */ -Resampler2::Resampler2 (Mode mode, - Precision precision, - bool use_sse_if_available) -{ - if (sse_available() && use_sse_if_available) - { - impl.reset (create_impl (mode, precision)); - } - else - { - impl.reset (create_impl (mode, precision)); - } -} - -bool -Resampler2::sse_available() -{ -#ifdef __SSE__ - return true; -#else - return false; -#endif -} - -Resampler2::Precision -Resampler2::find_precision_for_bits (uint bits) -{ - if (bits <= 1) - return PREC_LINEAR; - if (bits <= 8) - return PREC_48DB; - if (bits <= 12) - return PREC_72DB; - if (bits <= 16) - return PREC_96DB; - if (bits <= 20) - return PREC_120DB; - - /* thats the best precision we can deliver (and by the way also close to - * the best precision possible with floats anyway) - */ - return PREC_144DB; -} - -const char * -Resampler2::precision_name (Precision precision) -{ - switch (precision) - { - case PREC_LINEAR: return "linear interpolation"; - case PREC_48DB: return "8 bit (48dB)"; - case PREC_72DB: return "12 bit (72dB)"; - case PREC_96DB: return "16 bit (96dB)"; - case PREC_120DB: return "20 bit (120dB)"; - case PREC_144DB: return "24 bit (144dB)"; - default: return "unknown precision enum value"; - } -} - -namespace { // Anon - -/* --- coefficient sets for Resampler2 --- */ -/* halfband FIR filter for factor 2 resampling, created with octave - * - * design method: windowed sinc, using ultraspherical window - * - * coefficients = 32 - * x0 = 1.01065 - * alpha = 0.75 - * - * design criteria (44100 Hz => 88200 Hz): - * - * passband = [ 0, 18000 ] 1 - 2^-16 <= H(z) <= 1+2^-16 - * transition = [ 18000, 26100 ] - * stopband = [ 26100, 44100 ] | H(z) | <= -96 dB - * - * and for 48 kHz => 96 kHz: - * - * passband = [ 0, 19589 ] 1 - 2^-16 <= H(z) <= 1+2^-16 - * transition = [ 19588, 29386 ] - * stopband = [ 29386, 48000 ] | H(z) | <= -96 dB - * - * in order to keep the coefficient number down to 32, the filter - * does only "almost" fulfill the spec, but its really really close - * (no stopband ripple > -95 dB) - */ - -static const double halfband_fir_96db_coeffs[32] = -{ - -3.48616530828033e-05, - 0.000112877490936198, - -0.000278961878372482, - 0.000590495306376081, - -0.00112566995029848, - 0.00198635062559427, - -0.00330178798332932, - 0.00523534239035401, - -0.00799905465189065, - 0.0118867161189188, - -0.0173508611368417, - 0.0251928452706978, - -0.0370909694665106, - 0.057408291607388, - -0.102239638342325, - 0.317002929635456, - /* here, a 0.5 coefficient will be used */ - 0.317002929635456, - -0.102239638342325, - 0.0574082916073878, - -0.0370909694665105, - 0.0251928452706976, - -0.0173508611368415, - 0.0118867161189186, - -0.00799905465189052, - 0.0052353423903539, - -0.00330178798332923, - 0.00198635062559419, - -0.00112566995029842, - 0.000590495306376034, - -0.00027896187837245, - 0.000112877490936177, - -3.48616530827983e-05 -}; - -/* coefficients = 16 - * x0 = 1.013 - * alpha = 0.2 - */ -static const double halfband_fir_48db_coeffs[16] = -{ - -0.00270578824181636, - 0.00566964586625895, - -0.0106460585587187, - 0.0185209590435965, - -0.0310433957594089, - 0.0525722488176905, - -0.0991138314110143, - 0.315921760444802, - /* here, a 0.5 coefficient will be used */ - 0.315921760444802, - -0.0991138314110145, - 0.0525722488176907, - -0.031043395759409, - 0.0185209590435966, - -0.0106460585587187, - 0.00566964586625899, - -0.00270578824181638 -}; - -/* coefficients = 24 - * x0 = 1.0105 - * alpha = 0.93 - */ -static const double halfband_fir_72db_coeffs[24] = -{ - -0.0002622341634289771, - 0.0007380549701258316, - -0.001634275943268986, - 0.00315564206632209, - -0.005564668530702518, - 0.009207977968023688, - -0.0145854155294611, - 0.02253220964143239, - -0.03474055058489597, - 0.05556350980411048, - -0.1010616834297558, - 0.316597934725021, - /* here, a 0.5 coefficient will be used */ - 0.3165979347250216, - -0.1010616834297563, - 0.0555635098041109, - -0.03474055058489638, - 0.02253220964143274, - -0.01458541552946141, - 0.00920797796802395, - -0.005564668530702722, - 0.003155642066322248, - -0.001634275943269096, - 0.000738054970125897, - -0.0002622341634290046, -}; - -/* coefficients = 42 - * x0 = 1.0106 - * alpha = 0.8 - */ -static const double halfband_fir_120db_coeffs[42] = { - 2.359361930421347e-06, - -9.506281154947505e-06, - 2.748456705299089e-05, - -6.620621425709478e-05, - 0.0001411845354098405, - -0.0002752082937581387, - 0.0005000548069542907, - -0.0008581650926168509, - 0.001404290771748464, - -0.002207303823772437, - 0.003352696749689989, - -0.004946913550236211, - 0.007125821223639453, - -0.01007206140806936, - 0.01405163477932994, - -0.01949467352546547, - 0.02718899890919871, - -0.038810852733035, - 0.05873397010869939, - -0.1030762204838426, - 0.317288892550808, - /* here, a 0.5 coefficient will be used */ - 0.3172888925508079, - -0.1030762204838425, - 0.0587339701086993, - -0.03881085273303492, - 0.02718899890919862, - -0.01949467352546535, - 0.01405163477932982, - -0.01007206140806923, - 0.007125821223639309, - -0.004946913550236062, - 0.003352696749689839, - -0.00220730382377229, - 0.001404290771748321, - -0.0008581650926167192, - 0.0005000548069541726, - -0.0002752082937580344, - 0.0001411845354097548, - -6.620621425702783e-05, - 2.748456705294319e-05, - -9.506281154917077e-06, - 2.359361930409472e-06 -}; - -/* coefficients = 52 - * x0 = 1.0104 - * alpha = 0.8 - */ -static const double halfband_fir_144db_coeffs[52] = { - -1.841826652087099e-07, - 8.762360674826639e-07, - -2.867933918842901e-06, - 7.670965310712155e-06, - -1.795091436711159e-05, - 3.808294405088742e-05, - -7.483688716947913e-05, - 0.0001381756990743866, - -0.0002421379200249195, - 0.0004057667984715052, - -0.0006540521320531017, - 0.001018873594538604, - -0.001539987101083099, - 0.002266194978575507, - -0.003257014968854008, - 0.004585469100383752, - -0.006343174213238195, - 0.008650017657145861, - -0.01167305853124126, - 0.01566484143899151, - -0.02104586507283325, - 0.02859957136356252, - -0.04000402932277326, - 0.05964131775019404, - -0.1036437507243546, - 0.3174820359034792, - /* here, a 0.5 coefficient will be used */ - 0.3174820359034791, - -0.1036437507243545, - 0.05964131775019401, - -0.04000402932277325, - 0.0285995713635625, - -0.02104586507283322, - 0.01566484143899148, - -0.01167305853124122, - 0.008650017657145822, - -0.006343174213238157, - 0.004585469100383712, - -0.003257014968853964, - 0.002266194978575464, - -0.00153998710108306, - 0.001018873594538566, - -0.0006540521320530672, - 0.0004057667984714751, - -0.0002421379200248905, - 0.0001381756990743623, - -7.483688716946011e-05, - 3.808294405087123e-05, - -1.795091436709889e-05, - 7.670965310702215e-06, - -2.867933918835638e-06, - 8.762360674786308e-07, - -1.841826652067372e-07, -}; - -/* linear interpolation coefficients; barely useful for actual audio use, - * but useful for testing - */ -static const double halfband_fir_linear_coeffs[2] = { - 0.25, - /* here, a 0.5 coefficient will be used */ - 0.25, -}; - -/* - * FIR filter routine - * - * A FIR filter has the characteristic that it has a finite impulse response, - * and can be computed by convolution of the input signal with that finite - * impulse response. - * - * Thus, we use this for computing the output of the FIR filter - * - * output = input[0] * taps[0] + input[1] * taps[1] + ... + input[N-1] * taps[N-1] - * - * where input is the input signal, taps are the filter coefficients, in - * other texts sometimes called h[0]..h[N-1] (impulse response) or a[0]..a[N-1] - * (non recursive part of a digital filter), and N is the filter order. - */ -template static inline Accumulator -fir_process_one_sample (const float *input, - const float *taps, /* [0..order-1] */ - const uint order) -{ - Accumulator out = 0; - for (uint i = 0; i < order; i++) - out += input[i] * taps[i]; - return out; -} - -/* - * FIR filter routine for 4 samples simultaneously - * - * This routine produces (approximately) the same result as fir_process_one_sample - * but computes four consecutive output values at once using vectorized SSE - * instructions. Note that input and sse_taps need to be 16-byte aligned here. - * - * Also note that sse_taps is not a plain impulse response here, but a special - * version that needs to be computed with fir_compute_sse_taps. - */ -static inline void -fir_process_4samples_sse (const float *input, - const float *sse_taps, - const uint order, - float *out0, - float *out1, - float *out2, - float *out3) -{ -#ifdef __SSE__ - /* input and taps must be 16-byte aligned */ - const F4Vector *input_v = reinterpret_cast (input); - const F4Vector *sse_taps_v = reinterpret_cast (sse_taps); - F4Vector out0_v, out1_v, out2_v, out3_v; - - out0_v.v = _mm_mul_ps (input_v[0].v, sse_taps_v[0].v); - out1_v.v = _mm_mul_ps (input_v[0].v, sse_taps_v[1].v); - out2_v.v = _mm_mul_ps (input_v[0].v, sse_taps_v[2].v); - out3_v.v = _mm_mul_ps (input_v[0].v, sse_taps_v[3].v); - - for (uint i = 1; i < (order + 6) / 4; i++) - { - out0_v.v = _mm_add_ps (out0_v.v, _mm_mul_ps (input_v[i].v, sse_taps_v[i * 4 + 0].v)); - out1_v.v = _mm_add_ps (out1_v.v, _mm_mul_ps (input_v[i].v, sse_taps_v[i * 4 + 1].v)); - out2_v.v = _mm_add_ps (out2_v.v, _mm_mul_ps (input_v[i].v, sse_taps_v[i * 4 + 2].v)); - out3_v.v = _mm_add_ps (out3_v.v, _mm_mul_ps (input_v[i].v, sse_taps_v[i * 4 + 3].v)); - } - - *out0 = out0_v.f[0] + out0_v.f[1] + out0_v.f[2] + out0_v.f[3]; - *out1 = out1_v.f[0] + out1_v.f[1] + out1_v.f[2] + out1_v.f[3]; - *out2 = out2_v.f[0] + out2_v.f[1] + out2_v.f[2] + out2_v.f[3]; - *out3 = out3_v.f[0] + out3_v.f[1] + out3_v.f[2] + out3_v.f[3]; -#else - ASE_ASSERT_RETURN_UNREACHED(); -#endif -} - - -/* - * fir_compute_sse_taps takes a normal vector of FIR taps as argument and - * computes a specially scrambled version of these taps, ready to be used - * for SSE operations (by fir_process_4samples_sse). - * - * we require a special ordering of the FIR taps, to get maximum benefit of the SSE operations - * - * example: suppose the FIR taps are [ x1 x2 x3 x4 x5 x6 x7 x8 x9 ], then the SSE taps become - * - * [ x1 x2 x3 x4 0 x1 x2 x3 0 0 x1 x2 0 0 0 x1 <- for input[0] - * x5 x6 x7 x8 x4 x5 x6 x7 x3 x4 x5 x6 x2 x3 x4 x5 <- for input[1] - * x9 0 0 0 x8 x9 0 0 x7 x8 x9 0 x6 x7 x8 x9 ] <- for input[2] - * \------------/\-----------/\-----------/\-----------/ - * for out0 for out1 for out2 for out3 - * - * so that we can compute out0, out1, out2 and out3 simultaneously - * from input[0]..input[2] - */ -static inline std::vector -fir_compute_sse_taps (const std::vector& taps) -{ - const int order = taps.size(); - std::vector sse_taps ((order + 6) / 4 * 16); - - for (int j = 0; j < 4; j++) - for (int i = 0; i < order; i++) - { - int k = i + j; - sse_taps[(k / 4) * 16 + (k % 4) + j * 4] = taps[i]; - } - - return sse_taps; -} - -/* - * This function tests the SSEified FIR filter code (that is, the reordering - * done by fir_compute_sse_taps and the actual computation implemented in - * fir_process_4samples_sse). - * - * It prints diagnostic information, and returns true if the filter - * implementation works correctly, and false otherwise. The maximum filter - * order to be tested can be optionally specified as argument. - */ -static inline bool -fir_test_filter_sse (bool verbose, - const uint max_order = 64) -{ - int errors = 0; - if (verbose) - printout ("testing SSE filter implementation:\n\n"); - - for (uint order = 0; order < max_order; order++) - { - std::vector taps (order); - for (uint i = 0; i < order; i++) - taps[i] = i + 1; - - FastMemArray sse_taps (fir_compute_sse_taps (taps)); - if (verbose) - { - for (uint i = 0; i < sse_taps.size(); i++) - { - printout ("%3d", (int) (sse_taps[i] + 0.5)); - if (i % 4 == 3) - printout (" |"); - if (i % 16 == 15) - printout (" ||| upper bound = %d\n", (order + 6) / 4); - } - printout ("\n\n"); - } - - FastMemArray random_mem (order + 6); - for (uint i = 0; i < order + 6; i++) - random_mem[i] = 1.0 - rand() / (0.5 * RAND_MAX); - - /* FIXME: the problem with this test is that we explicitely test SSE code - * here, but the test case is not compiled with -msse within the BEAST tree - */ - float out[4]; - fir_process_4samples_sse (&random_mem[0], &sse_taps[0], order, - &out[0], &out[1], &out[2], &out[3]); - - double avg_diff = 0.0; - for (int i = 0; i < 4; i++) - { - double diff = fir_process_one_sample (&random_mem[i], &taps[0], order) - out[i]; - avg_diff += fabs (diff); - } - avg_diff /= (order + 1); - bool is_error = (avg_diff > 0.00001); - if (is_error || verbose) - printout ("*** order = %d, avg_diff = %g\n", order, avg_diff); - if (is_error) - errors++; - } - if (errors) - printout ("*** %d errors detected\n", errors); - - return (errors == 0); -} - -} // Anon - -/* - * Factor 2 upsampling of a data stream - * - * Template arguments: - * ORDER number of resampling filter coefficients - * USE_SSE whether to use SSE (vectorized) instructions or not - */ -template -class Resampler2::Upsampler2 final : public Resampler2::Impl { - std::vector taps; - FastMemArray history; - FastMemArray sse_taps; -protected: - /* fast SSE optimized convolution */ - void - process_4samples_aligned (const float *input /* aligned */, - float *output) - { - const uint H = (ORDER / 2); /* half the filter length */ - - output[1] = input[H]; - output[3] = input[H + 1]; - output[5] = input[H + 2]; - output[7] = input[H + 3]; - - fir_process_4samples_sse (input, &sse_taps[0], ORDER, &output[0], &output[2], &output[4], &output[6]); - } - /* slow convolution */ - void - process_sample_unaligned (const float *input, - float *output) - { - const uint H = (ORDER / 2); /* half the filter length */ - output[0] = fir_process_one_sample (&input[0], &taps[0], ORDER); - output[1] = input[H]; - } - void - process_block_aligned (const float *input, - uint n_input_samples, - float *output) - { - uint i = 0; - if (USE_SSE) - { - while (i + 3 < n_input_samples) - { - process_4samples_aligned (&input[i], &output[i*2]); - i += 4; - } - } - while (i < n_input_samples) - { - process_sample_unaligned (&input[i], &output[2*i]); - i++; - } - } - void - process_block_unaligned (const float *input, - uint n_input_samples, - float *output) - { - uint i = 0; - if (USE_SSE) - { - while ((reinterpret_cast (&input[i]) & 15) && i < n_input_samples) - { - process_sample_unaligned (&input[i], &output[2 * i]); - i++; - } - } - process_block_aligned (&input[i], n_input_samples - i, &output[2 * i]); - } -public: - /* - * Constructs an Upsampler2 object with a given set of filter coefficients. - * - * init_taps: coefficients for the upsampling FIR halfband filter - */ - Upsampler2 (float *init_taps) : - taps (init_taps, init_taps + ORDER), - history (2 * ORDER), - sse_taps (fir_compute_sse_taps (taps)) - { - ASE_ASSERT_RETURN ((ORDER & 1) == 0); /* even order filter */ - } - /* - * The function process_block() takes a block of input samples and produces a - * block with twice the length, containing interpolated output samples. - */ - void - process_block (const float *input, - uint n_input_samples, - float *output) override - { - const uint history_todo = min (n_input_samples, ORDER - 1); - - copy (input, input + history_todo, &history[ORDER - 1]); - process_block_aligned (&history[0], history_todo, output); - if (n_input_samples > history_todo) - { - process_block_unaligned (input, n_input_samples - history_todo, &output [2 * history_todo]); - - // build new history from new input - copy (input + n_input_samples - history_todo, input + n_input_samples, &history[0]); - } - else - { - // build new history from end of old history - // (very expensive if n_input_samples tends to be a lot smaller than ORDER often) - memmove (&history[0], &history[n_input_samples], sizeof (history[0]) * (ORDER - 1)); - } - } - /* - * Returns the FIR filter order. - */ - uint - order() const override - { - return ORDER; - } - double - delay() const override - { - return order() - 1; - } - void - reset() override - { - floatfill (&history[0], 0.0, history.size()); - } - bool - sse_enabled() const override - { - return USE_SSE; - } -}; - -/* - * Factor 2 downsampling of a data stream - * - * Template arguments: - * ORDER number of resampling filter coefficients - * USE_SSE whether to use SSE (vectorized) instructions or not - */ -template -class Resampler2::Downsampler2 final : public Resampler2::Impl { - std::vector taps; - FastMemArray history_even; - FastMemArray history_odd; - FastMemArray sse_taps; - /* fast SSE optimized convolution */ - template void - process_4samples_aligned (const float *input_even /* aligned */, - const float *input_odd, - float *output) - { - const uint H = (ORDER / 2) - 1; /* half the filter length */ - - fir_process_4samples_sse (input_even, &sse_taps[0], ORDER, &output[0], &output[1], &output[2], &output[3]); - - output[0] += 0.5f * input_odd[H * ODD_STEPPING]; - output[1] += 0.5f * input_odd[(H + 1) * ODD_STEPPING]; - output[2] += 0.5f * input_odd[(H + 2) * ODD_STEPPING]; - output[3] += 0.5f * input_odd[(H + 3) * ODD_STEPPING]; - } - /* slow convolution */ - template float - process_sample_unaligned (const float *input_even, - const float *input_odd) - { - const uint H = (ORDER / 2) - 1; /* half the filter length */ - - return fir_process_one_sample (&input_even[0], &taps[0], ORDER) + 0.5f * input_odd[H * ODD_STEPPING]; - } - template void - process_block_aligned (const float *input_even, - const float *input_odd, - float *output, - uint n_output_samples) - { - uint i = 0; - if (USE_SSE) - { - while (i + 3 < n_output_samples) - { - process_4samples_aligned (&input_even[i], &input_odd[i * ODD_STEPPING], &output[i]); - i += 4; - } - } - while (i < n_output_samples) - { - output[i] = process_sample_unaligned (&input_even[i], &input_odd[i * ODD_STEPPING]); - i++; - } - } - template void - process_block_unaligned (const float *input_even, - const float *input_odd, - float *output, - uint n_output_samples) - { - uint i = 0; - if (USE_SSE) - { - while ((reinterpret_cast (&input_even[i]) & 15) && i < n_output_samples) - { - output[i] = process_sample_unaligned (&input_even[i], &input_odd[i * ODD_STEPPING]); - i++; - } - } - process_block_aligned (&input_even[i], &input_odd[i * ODD_STEPPING], &output[i], n_output_samples); - } - void - deinterleave2 (const float *data, - uint n_data_values, - float *output) - { - for (uint i = 0; i < n_data_values; i += 2) - output[i / 2] = data[i]; - } -public: - /* - * Constructs a Downsampler2 class using a given set of filter coefficients. - * - * init_taps: coefficients for the downsampling FIR halfband filter - */ - Downsampler2 (float *init_taps) : - taps (init_taps, init_taps + ORDER), - history_even (2 * ORDER), - history_odd (2 * ORDER), - sse_taps (fir_compute_sse_taps (taps)) - { - ASE_ASSERT_RETURN ((ORDER & 1) == 0); /* even order filter */ - } - /* - * The function process_block() takes a block of input samples and produces - * a block with half the length, containing downsampled output samples. - */ - void - process_block (const float *input, - uint n_input_samples, - float *output) override - { - ASE_ASSERT_RETURN ((n_input_samples & 1) == 0); - - const uint BLOCKSIZE = 1024; - - F4Vector block[BLOCKSIZE / 4]; /* using F4Vector ensures 16-byte alignment */ - float *input_even = &block[0].f[0]; - - while (n_input_samples) - { - uint n_input_todo = min (n_input_samples, BLOCKSIZE * 2); - - /* since the halfband filter contains zeros every other sample - * and since we're using SSE instructions, which expect the - * data to be consecutively represented in memory, we prepare - * a block of samples containing only even-indexed samples - * - * we keep the deinterleaved data on the stack (instead of per-class - * allocated memory), to ensure that even running a lot of these - * downsampler streams will not result in cache trashing - * - * FIXME: this implementation is suboptimal for non-SSE, because it - * performs an extra deinterleaving step in any case, but deinterleaving - * is only required for SSE instructions - */ - deinterleave2 (input, n_input_todo, input_even); - - const float *input_odd = input + 1; /* we process this one with a stepping of 2 */ - - const uint n_output_todo = n_input_todo / 2; - const uint history_todo = min (n_output_todo, ORDER - 1); - - copy (input_even, input_even + history_todo, &history_even[ORDER - 1]); - deinterleave2 (input_odd, history_todo * 2, &history_odd[ORDER - 1]); - - process_block_aligned <1> (&history_even[0], &history_odd[0], output, history_todo); - if (n_output_todo > history_todo) - { - process_block_unaligned<2> (input_even, input_odd, &output[history_todo], n_output_todo - history_todo); - - // build new history from new input (here: history_todo == ORDER - 1) - copy (input_even + n_output_todo - history_todo, input_even + n_output_todo, &history_even[0]); - deinterleave2 (input_odd + n_input_todo - history_todo * 2, history_todo * 2, &history_odd[0]); /* FIXME: can be optimized */ - } - else - { - // build new history from end of old history - // (very expensive if n_output_todo tends to be a lot smaller than ORDER often) - memmove (&history_even[0], &history_even[n_output_todo], sizeof (history_even[0]) * (ORDER - 1)); - memmove (&history_odd[0], &history_odd[n_output_todo], sizeof (history_odd[0]) * (ORDER - 1)); - } - - n_input_samples -= n_input_todo; - input += n_input_todo; - output += n_output_todo; - } - } - /* - * Returns the filter order. - */ - uint - order() const override - { - return ORDER; - } - double - delay() const override - { - return order() / 2 - 0.5; - } - void - reset() override - { - floatfill (&history_even[0], 0.0, history_even.size()); - floatfill (&history_odd[0], 0.0, history_odd.size()); - } - bool - sse_enabled() const override - { - return USE_SSE; - } -}; - -template Resampler2::Impl* -Resampler2::create_impl (Mode mode, - Precision precision) -{ - if (mode == UP) - { - switch (precision) - { - case PREC_LINEAR: return create_impl_with_coeffs > (halfband_fir_linear_coeffs, 2, 2.0); - case PREC_48DB: return create_impl_with_coeffs > (halfband_fir_48db_coeffs, 16, 2.0); - case PREC_72DB: return create_impl_with_coeffs > (halfband_fir_72db_coeffs, 24, 2.0); - case PREC_96DB: return create_impl_with_coeffs > (halfband_fir_96db_coeffs, 32, 2.0); - case PREC_120DB: return create_impl_with_coeffs > (halfband_fir_120db_coeffs, 42, 2.0); - case PREC_144DB: return create_impl_with_coeffs > (halfband_fir_144db_coeffs, 52, 2.0); - } - } - else if (mode == DOWN) - { - switch (precision) - { - case PREC_LINEAR: return create_impl_with_coeffs > (halfband_fir_linear_coeffs, 2, 1.0); - case PREC_48DB: return create_impl_with_coeffs > (halfband_fir_48db_coeffs, 16, 1.0); - case PREC_72DB: return create_impl_with_coeffs > (halfband_fir_72db_coeffs, 24, 1.0); - case PREC_96DB: return create_impl_with_coeffs > (halfband_fir_96db_coeffs, 32, 1.0); - case PREC_120DB: return create_impl_with_coeffs > (halfband_fir_120db_coeffs, 42, 1.0); - case PREC_144DB: return create_impl_with_coeffs > (halfband_fir_144db_coeffs, 52, 1.0); - } - } - return 0; -} - -bool -Resampler2::test_filter_impl (bool verbose) -{ - if (sse_available()) - { - return fir_test_filter_sse (verbose); - } - else - { - printerr ("SSE filter implementation not tested: no SSE support available\n"); - return true; - } -} - -} // Ase - -#include "testing.hh" - -namespace { // Anon -using namespace Ase; - -TEST_INTEGRITY (resampler2_tests); -static void -resampler2_tests() -{ - const bool verbose = false; - const bool filter_ok = Resampler2::test_filter_impl (verbose); - TASSERT (filter_ok); -} - -} // Anon diff --git a/ase/resampler2.hh b/ase/resampler2.hh deleted file mode 100644 index 63e649a4..00000000 --- a/ase/resampler2.hh +++ /dev/null @@ -1,148 +0,0 @@ -// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 -#ifndef __ASE_RESAMPLER_HH__ -#define __ASE_RESAMPLER_HH__ - -#include - -namespace Ase { - -/** - * Interface for factor 2 resampling classes - */ -class Resampler2 { - class Impl - { - public: - virtual void process_block (const float *input, uint n_input_samples, float *output) = 0; - virtual uint order() const = 0; - virtual double delay() const = 0; - virtual void reset() = 0; - virtual bool sse_enabled() const = 0; - virtual - ~Impl() - { - } - }; - std::unique_ptr impl; - - template - class Upsampler2; - template - class Downsampler2; -public: - enum Mode { - UP, - DOWN - }; - enum Precision { - PREC_LINEAR = 1, /* linear interpolation */ - PREC_48DB = 8, - PREC_72DB = 12, - PREC_96DB = 16, - PREC_120DB = 20, - PREC_144DB = 24 - }; - /** - * creates a resampler instance fulfilling a given specification - */ - Resampler2 (Mode mode, - Precision precision, - bool use_sse_if_available = true); - /** - * returns true if an optimized SSE version of the Resampler is available - */ - static bool sse_available(); - /** - * test internal filter implementation - */ - static bool test_filter_impl (bool verbose); - /** - * finds a precision which is appropriate for at least the specified number of bits - */ - static Precision find_precision_for_bits (uint bits); - /** - * returns a human-readable name for a given precision - */ - static const char *precision_name (Precision precision); - /** - * resample a data block - */ - void - process_block (const float *input, uint n_input_samples, float *output) - { - impl->process_block (input, n_input_samples, output); - } - /** - * return FIR filter order - */ - uint - order() const - { - return impl->order(); - } - /** - * Return the delay introduced by the resampler. This delay is guaranteed to - * be >= 0.0, and for factor 2 resampling always a multiple of 0.5 (1.0 for - * upsampling). - * - * The return value can also be thought of as index into the output signal, - * where the first input sample can be found. - * - * Beware of fractional delays, for instance for downsampling, a delay() of - * 10.5 means that the first input sample would be found by interpolating - * output[10] and output[11], and the second input sample equates output[11]. - */ - double - delay() const - { - return impl->delay(); - } - /** - * clear internal history, reset resampler state to zero values - */ - void - reset() - { - impl->reset(); - } - /** - * return whether the resampler is using sse optimized code - */ - bool - sse_enabled() const - { - return impl->sse_enabled(); - } -protected: - /* Creates implementation from filter coefficients and Filter implementation class - * - * Since up- and downsamplers use different (scaled) coefficients, its possible - * to specify a scaling factor. Usually 2 for upsampling and 1 for downsampling. - */ - template static inline Impl* - create_impl_with_coeffs (const double *d, - uint order, - double scaling) - { - float taps[order]; - for (uint i = 0; i < order; i++) - taps[i] = d[i] * scaling; - - Resampler2::Impl *filter = new Filter (taps); - ASE_ASSERT_RETURN (order == filter->order(), NULL); - return filter; - } - /* creates the actual implementation; specifying USE_SSE=true will use - * SSE instructions, USE_SSE=false will use FPU instructions - * - * Don't use this directly - it's only to be used by - * aseblockutils.cc's anonymous Impl classes. - */ - template static inline Impl* - create_impl (Mode mode, - Precision precision); -}; - -} // Ase - -#endif /* __ASE_RESAMPLER_HH__ */