Line data Source code
1 : #include "audio/effects/pitch/octaver.h"
2 :
3 : #include <cmath>
4 :
5 : #include "audio/effects/core/effect_factory.h"
6 :
7 : namespace Amplitron {
8 :
9 2 : static EffectRegistrar<Octaver> reg("Octaver");
10 :
11 : // Param indices
12 : static constexpr int P_OCT_DOWN = 0;
13 : static constexpr int P_OCT_UP = 1;
14 : static constexpr int P_DRY = 2;
15 :
16 : // Hysteresis threshold for the flip-flop zero-crossing detector.
17 : // The divider only flips when prev_sample_ < -FLIP_HYSTERESIS and the current
18 : // sample > +FLIP_HYSTERESIS, preventing chatter caused by near-zero noise.
19 : //
20 : // Value choice: 0.002 ≈ -54 dBFS — comfortably above the noise floor of a
21 : // typical USB audio interface while remaining well below the smallest musically
22 : // significant guitar signal. At this threshold the per-sample slope of the
23 : // slowest guitar fundamental (E2 ≈ 82 Hz at 48 kHz) still crosses the ±H band
24 : // reliably, so the divider tracks pitch without false negatives. Tune upward
25 : // only if the target hardware has an unusually noisy input stage.
26 : static constexpr float FLIP_HYSTERESIS = 0.002f;
27 :
28 52 : Octaver::Octaver() {
29 117 : params_ = {
30 39 : {"Oct -1", 0.5f, 0.0f, 1.0f, 0.5f, "",
31 13 : "Level of the sub-octave (one octave below). Produces a thick, organ-like low tone."},
32 13 : {"Oct +1", 0.0f, 0.0f, 1.0f, 0.0f, "",
33 13 : "Level of the upper octave (one octave above). Adds a bright, shimmery harmonic."},
34 13 : {"Dry", 0.7f, 0.0f, 1.0f, 0.7f, "",
35 13 : "Level of the original dry signal blended with the octave voices."},
36 117 : };
37 52 : set_sample_rate(DEFAULT_SAMPLE_RATE);
38 78 : }
39 :
40 75 : void Octaver::set_sample_rate(int sample_rate) {
41 75 : Effect::set_sample_rate(sample_rate);
42 75 : reset();
43 62 : }
44 :
45 408 : void Octaver::process(float* buffer, int num_samples) {
46 408 : if (!enabled_) return;
47 :
48 : // One-pole smoothing coefficient (~10 ms time constant)
49 405 : const float alpha = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.010f));
50 :
51 : // Envelope follower coefficients
52 405 : const float env_attack = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.002f)); // 2 ms
53 405 : const float env_release = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.020f)); // 20 ms
54 :
55 : // DC blocker coefficient (high-pass at ~20 Hz)
56 405 : const float dc_coeff = 1.0f - (TWO_PI * 20.0f / sample_rate_);
57 :
58 205269 : for (int i = 0; i < num_samples; ++i) {
59 204864 : const float dry = buffer[i];
60 :
61 : // Smooth parameters
62 204864 : oct_down_smooth_ += alpha * (params_[P_OCT_DOWN].value - oct_down_smooth_);
63 204864 : oct_up_smooth_ += alpha * (params_[P_OCT_UP].value - oct_up_smooth_);
64 204864 : dry_smooth_ += alpha * (params_[P_DRY].value - dry_smooth_);
65 :
66 : // --- Envelope follower (tracks input amplitude) ---
67 204864 : const float abs_in = std::fabs(dry);
68 204864 : if (abs_in > envelope_) {
69 79845 : envelope_ += env_attack * (abs_in - envelope_);
70 26615 : } else {
71 125019 : envelope_ += env_release * (abs_in - envelope_);
72 : }
73 :
74 : // --- Oct-1: Flip-flop divider ---
75 : // Detect positive-going zero crossing with hysteresis: require the
76 : // previous sample to have been clearly negative and the current sample
77 : // to be clearly positive before toggling. This suppresses chatter from
78 : // near-zero noise that would otherwise cause false flips.
79 204864 : if (prev_sample_ < -FLIP_HYSTERESIS && dry > FLIP_HYSTERESIS) {
80 1899 : flipflop_ = -flipflop_;
81 633 : }
82 204864 : prev_sample_ = dry;
83 :
84 : // Square wave at half frequency, shaped by envelope
85 204864 : float oct_down = flipflop_ * envelope_;
86 :
87 : // --- Oct+1: Full-wave rectification + DC removal ---
88 204864 : float rectified = std::fabs(dry);
89 :
90 : // DC blocker (1st-order high-pass): removes the DC offset from rectification
91 204864 : float dc_out = rectified - dc_x1_ + dc_coeff * dc_y1_;
92 204864 : dc_x1_ = rectified;
93 204864 : dc_y1_ = dc_out;
94 :
95 204864 : float oct_up = dc_out;
96 :
97 : // --- Mix ---
98 204864 : buffer[i] = dry * dry_smooth_ + oct_down * oct_down_smooth_ + oct_up * oct_up_smooth_;
99 :
100 : // Safety clamp
101 204864 : buffer[i] = clamp(buffer[i], -1.0f, 1.0f);
102 68288 : }
103 136 : }
104 :
105 192 : void Octaver::reset() {
106 192 : prev_sample_ = 0.0f;
107 192 : flipflop_ = 1.0f;
108 192 : dc_x1_ = 0.0f;
109 192 : dc_y1_ = 0.0f;
110 192 : envelope_ = 0.0f;
111 192 : oct_down_smooth_ = params_[P_OCT_DOWN].value;
112 192 : oct_up_smooth_ = params_[P_OCT_UP].value;
113 192 : dry_smooth_ = params_[P_DRY].value;
114 192 : }
115 :
116 : } // namespace Amplitron
|