SpectMorph
smladdervcf.hh
1 // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
2 
3 #pragma once
4 
5 #include "smpandaresampler.hh"
6 
7 #include <array>
8 #include <algorithm>
9 #include <cassert>
10 #include <cmath>
11 
12 namespace SpectMorph {
13 
15 
16 class LadderVCF
17 {
18 public:
19  enum Mode {
20  LP1, LP2, LP3, LP4
21  };
22 private:
23  struct Channel {
24  float x1, x2, x3, x4;
25  float y1, y2, y3, y4;
26 
27  std::unique_ptr<Resampler2> res_up;
28  std::unique_ptr<Resampler2> res_down;
29  };
30  std::array<Channel, 2> channels_;
31  Mode mode_;
32  float rate_ = 0;
33  float freq_scale_factor_ = 0;
34  float frequency_range_min_ = 0;
35  float frequency_range_max_ = 0;
36  float clamp_freq_min_ = 0;
37  float clamp_freq_max_ = 0;
38  float freq_ = 440;
39  float reso_ = 0;
40  float drive_ = 0;
41  float global_volume_ = 1;
42  uint over_ = 0;
43  bool test_linear_ = false;
44 
45  static constexpr uint MAX_BLOCK_SIZE = 1024;
46 
47  struct FParams
48  {
49  float reso = 0;
50  float pre_scale = 1;
51  float post_scale = 1;
52  };
53  FParams fparams_;
54  bool fparams_valid_ = false;
55 public:
56  LadderVCF (int over) :
57  over_ (over)
58  {
59  for (auto& channel : channels_)
60  {
61  channel.res_up = std::make_unique<Resampler2> (Resampler2::UP, over_, Resampler2::PREC_72DB);
62  channel.res_down = std::make_unique<Resampler2> (Resampler2::DOWN, over_, Resampler2::PREC_72DB);
63  }
64  set_mode (Mode::LP4);
65  set_rate (48000);
66  set_frequency_range (10, 24000);
67  reset();
68  }
69  void
70  set_mode (Mode new_mode)
71  {
72  mode_ = new_mode;
73  }
74  void
75  set_freq (float freq)
76  {
77  freq_ = freq;
78  }
79  void
80  set_reso (float reso)
81  {
82  reso_ = reso;
83  fparams_valid_ = false;
84  }
85  void
86  set_drive (float drive)
87  {
88  drive_ = drive;
89  fparams_valid_ = false;
90  }
91  void
92  set_global_volume (float global_volume)
93  {
94  /* every samples that is processed by the filter is
95  * - multiplied with global_volume before processing
96  * - divided by global_volume after processing
97  * which has an effect on the non-linear part of the filter (drive)
98  */
99  global_volume_ = global_volume;
100  fparams_valid_ = false;
101  }
102  void
103  set_test_linear (bool test_linear)
104  {
105  test_linear_ = test_linear;
106  fparams_valid_ = false;
107  }
108  void
109  set_rate (float r)
110  {
111  rate_ = r;
112  freq_scale_factor_ = 2 * M_PI / (rate_ * over_);
113 
114  update_frequency_range();
115  }
116  void
117  set_frequency_range (float min_freq, float max_freq)
118  {
119  frequency_range_min_ = min_freq;
120  frequency_range_max_ = max_freq;
121 
122  update_frequency_range();
123  }
124  void
125  reset()
126  {
127  for (auto& c : channels_)
128  {
129  c.x1 = c.x2 = c.x3 = c.x4 = 0;
130  c.y1 = c.y2 = c.y3 = c.y4 = 0;
131 
132  c.res_up->reset();
133  c.res_down->reset();
134  }
135  fparams_valid_ = false;
136  }
137  double
138  delay()
139  {
140  return channels_[0].res_up->delay() / over_ + channels_[0].res_down->delay();
141  }
142 private:
143  void
144  update_frequency_range()
145  {
146  /* we want to clamp to the user defined range (set_frequency_range())
147  * but also enforce that the filter is well below nyquist frequency
148  */
149  clamp_freq_min_ = frequency_range_min_;
150  clamp_freq_max_ = std::min (frequency_range_max_, rate_ * over_ * 0.49f);
151  }
152  void
153  setup_reso_drive (FParams& fparams, float reso, float drive)
154  {
155  reso = std::clamp (reso, 0.001f, 1.f);
156 
157  if (test_linear_) // test filter as linear filter; don't do any resonance correction
158  {
159  const float scale = 1e-5;
160  fparams.pre_scale = scale;
161  fparams.post_scale = 1 / scale;
162  fparams.reso = reso * 4;
163 
164  return;
165  }
166  const float db_x2_factor = 0.166096404744368; // 1/(20*log(2)/log(10))
167 
168  // scale signal down (without normalization on output) for negative drive
169  float negative_drive_vol = 1;
170  if (drive < 0)
171  {
172  negative_drive_vol = exp2f (drive * db_x2_factor);
173  drive = 0;
174  }
175  // drive resonance boost
176  if (drive > 0)
177  reso += drive * sqrt (reso) * reso * 0.03f;
178 
179  float vol = exp2f ((drive + -12 * sqrt (reso)) * db_x2_factor);
180  fparams.pre_scale = negative_drive_vol * vol * global_volume_;
181  fparams.post_scale = std::max (1 / vol, 1.0f) / global_volume_;
182  fparams.reso = sqrt (reso) * 4;
183  }
184  static float
185  tanh_approx (float x)
186  {
187  // https://www.musicdsp.org/en/latest/Other/238-rational-tanh-approximation.html
188  x = std::clamp (x, -3.0f, 3.0f);
189 
190  return x * (27.0f + x * x) / (27.0f + 9.0f * x * x);
191  }
192  /*
193  * This ladder filter implementation is mainly based on
194  *
195  * Välimäki, Vesa & Huovilainen, Antti. (2006).
196  * Oscillator and Filter Algorithms for Virtual Analog Synthesis.
197  * Computer Music Journal. 30. 19-31. 10.1162/comj.2006.30.2.19.
198  */
199  template<Mode MODE, bool STEREO> inline void
200  run (float *left, float *right, float freq, uint n_samples)
201  {
202  const float fc = std::clamp (freq, clamp_freq_min_, clamp_freq_max_) * freq_scale_factor_;
203  const float g = 0.9892f * fc - 0.4342f * fc * fc + 0.1381f * fc * fc * fc - 0.0202f * fc * fc * fc * fc;
204  const float b0 = g * (1 / 1.3f);
205  const float b1 = g * (0.3f / 1.3f);
206  const float a1 = g - 1;
207 
208  float res = fparams_.reso;
209  res *= 1.0029f + 0.0526f * fc - 0.0926f * fc * fc + 0.0218f * fc * fc * fc;
210 
211  for (uint os = 0; os < n_samples; os++)
212  {
213  for (uint i = 0; i < (STEREO ? 2 : 1); i++)
214  {
215  float &value = i == 0 ? left[os] : right[os];
216 
217  Channel& c = channels_[i];
218  const float x = value * fparams_.pre_scale;
219  const float g_comp = 0.5f; // passband gain correction
220  const float x0 = tanh_approx (x - (c.y4 - g_comp * x) * res);
221 
222  c.y1 = b0 * x0 + b1 * c.x1 - a1 * c.y1;
223  c.x1 = x0;
224 
225  c.y2 = b0 * c.y1 + b1 * c.x2 - a1 * c.y2;
226  c.x2 = c.y1;
227 
228  c.y3 = b0 * c.y2 + b1 * c.x3 - a1 * c.y3;
229  c.x3 = c.y2;
230 
231  c.y4 = b0 * c.y3 + b1 * c.x4 - a1 * c.y4;
232  c.x4 = c.y3;
233 
234  switch (MODE)
235  {
236  case LP1:
237  value = c.y1 * fparams_.post_scale;
238  break;
239  case LP2:
240  value = c.y2 * fparams_.post_scale;
241  break;
242  case LP3:
243  value = c.y3 * fparams_.post_scale;
244  break;
245  case LP4:
246  value = c.y4 * fparams_.post_scale;
247  break;
248  default:
249  assert (false);
250  }
251  }
252  }
253  }
254  template<Mode MODE, bool STEREO> inline void
255  do_process_block (uint n_samples,
256  float *left,
257  float *right,
258  const float *freq_in,
259  const float *reso_in,
260  const float *drive_in)
261  {
262  float over_samples_left[over_ * n_samples];
263  float over_samples_right[over_ * n_samples];
264 
265  channels_[0].res_up->process_block (left, n_samples, over_samples_left);
266  if (STEREO)
267  channels_[1].res_up->process_block (right, n_samples, over_samples_right);
268 
269  if (!fparams_valid_)
270  {
271  setup_reso_drive (fparams_, reso_in ? reso_in[0] : reso_, drive_in ? drive_in[0] : drive_);
272  fparams_valid_ = true;
273  }
274 
275  if (reso_in || drive_in)
276  {
277  /* for reso or drive modulation, we split the input it into small blocks
278  * and interpolate the pre_scale / post_scale / reso parameters
279  */
280  float *left_blk = over_samples_left;
281  float *right_blk = over_samples_right;
282 
283  uint n_remaining_samples = n_samples;
284  while (n_remaining_samples)
285  {
286  const uint todo = std::min<uint> (n_remaining_samples, 64);
287 
288  FParams fparams_end;
289  setup_reso_drive (fparams_end, reso_in ? reso_in[todo - 1] : reso_, drive_in ? drive_in[todo - 1] : drive_);
290 
291  float todo_inv = 1.f / todo;
292  float delta_pre_scale = (fparams_end.pre_scale - fparams_.pre_scale) * todo_inv;
293  float delta_post_scale = (fparams_end.post_scale - fparams_.post_scale) * todo_inv;
294  float delta_reso = (fparams_end.reso - fparams_.reso) * todo_inv;
295 
296  uint j = 0;
297  for (uint i = 0; i < todo * over_; i += over_)
298  {
299  fparams_.pre_scale += delta_pre_scale;
300  fparams_.post_scale += delta_post_scale;
301  fparams_.reso += delta_reso;
302 
303  float freq = freq_in ? freq_in[j++] : freq_;
304 
305  run<MODE, STEREO> (left_blk + i, right_blk + i, freq, over_);
306  }
307 
308  n_remaining_samples -= todo;
309  left_blk += todo * over_;
310  right_blk += todo * over_;
311 
312  if (freq_in)
313  freq_in += todo;
314  if (reso_in)
315  reso_in += todo;
316  if (drive_in)
317  drive_in += todo;
318  }
319  }
320  else if (freq_in)
321  {
322  uint over_pos = 0;
323 
324  for (uint i = 0; i < n_samples; i++)
325  {
326  run<MODE, STEREO> (over_samples_left + over_pos, over_samples_right + over_pos, freq_in[i], over_);
327  over_pos += over_;
328  }
329  }
330  else
331  {
332  run<MODE, STEREO> (over_samples_left, over_samples_right, freq_, n_samples * over_);
333  }
334  channels_[0].res_down->process_block (over_samples_left, over_ * n_samples, left);
335  if (STEREO)
336  channels_[1].res_down->process_block (over_samples_right, over_ * n_samples, right);
337  }
338  template<Mode MODE> inline void
339  process_block_mode (uint n_samples,
340  float *left,
341  float *right,
342  const float *freq_in,
343  const float *reso_in,
344  const float *drive_in)
345  {
346  if (right) // stereo?
347  do_process_block<MODE, true> (n_samples, left, right, freq_in, reso_in, drive_in);
348  else
349  do_process_block<MODE, false> (n_samples, left, right, freq_in, reso_in, drive_in);
350  }
351 public:
352  void
353  process_block (uint n_samples,
354  float *left,
355  float *right = nullptr,
356  const float *freq_in = nullptr,
357  const float *reso_in = nullptr,
358  const float *drive_in = nullptr)
359  {
360  while (n_samples)
361  {
362  const uint todo = std::min (n_samples, MAX_BLOCK_SIZE);
363 
364  switch (mode_)
365  {
366  case LP4: process_block_mode<LP4> (todo, left, right, freq_in, reso_in, drive_in);
367  break;
368  case LP3: process_block_mode<LP3> (todo, left, right, freq_in, reso_in, drive_in);
369  break;
370  case LP2: process_block_mode<LP2> (todo, left, right, freq_in, reso_in, drive_in);
371  break;
372  case LP1: process_block_mode<LP1> (todo, left, right, freq_in, reso_in, drive_in);
373  break;
374  }
375 
376  if (left)
377  left += todo;
378  if (right)
379  right += todo;
380  if (freq_in)
381  freq_in += todo;
382  if (reso_in)
383  reso_in += todo;
384  if (drive_in)
385  drive_in += todo;
386 
387  n_samples -= todo;
388  }
389  }
390 };
391 
392 } // SpectMorph
Definition: smpandaresampler.hh:99
Definition: smladdervcf.hh:17