Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
headphone_crossfeed.hpp
Go to the documentation of this file.
1#pragma once
2
5
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <cstddef>
12#include <limits>
13#include <numbers>
14#include <type_traits>
15
16namespace pulp::signal {
17
24template <typename SampleType = float> class HeadphoneCrossfeedT {
25 public:
26 static_assert(std::is_floating_point_v<SampleType>);
27
28 static constexpr double kMinimumSampleRate = 1000.0;
29 static constexpr double kMaximumSampleRate = 768000.0;
30 static constexpr double kMaximumDelayMs = 1.0;
31 static constexpr double kMaximumCrossfeedGain = 0.35;
32
34 update_derived_parameters();
35 }
36
37 bool prepare(double sample_rate) noexcept {
38 if (!std::isfinite(sample_rate) || sample_rate < kMinimumSampleRate ||
40 return false;
41 sample_rate_ = sample_rate;
42 update_derived_parameters();
43 reset();
44 return true;
45 }
46
47 double sample_rate() const noexcept {
48 return sample_rate_;
49 }
50
51 void set_enabled(bool enabled) noexcept {
52 enabled_ = enabled;
53 }
54 bool enabled() const noexcept {
55 return enabled_;
56 }
57
60 void set_amount(double amount01) noexcept {
61 if (!std::isfinite(amount01))
62 return;
63 amount_ = std::clamp(amount01, 0.0, 1.0);
64 update_mix();
65 }
66 double amount() const noexcept {
67 return amount_;
68 }
69 double crossfeed_gain() const noexcept {
70 return amount_ * kMaximumCrossfeedGain;
71 }
72
74 void set_delay_ms(double delay_ms) noexcept {
75 if (!std::isfinite(delay_ms))
76 return;
77 delay_ms_ = std::clamp(delay_ms, 0.0, kMaximumDelayMs);
78 update_delay_samples();
79 }
80 double delay_ms() const noexcept {
81 return delay_ms_;
82 }
83 double delay_samples() const noexcept {
84 return delay_samples_;
85 }
86
89 void set_cutoff_hz(double cutoff_hz) noexcept {
90 if (!std::isfinite(cutoff_hz))
91 return;
92 requested_cutoff_hz_ = std::clamp(cutoff_hz, 100.0, 20000.0);
93 update_filter();
94 }
95 double cutoff_hz() const noexcept {
96 return cutoff_hz_;
97 }
98
101 static constexpr int latency_samples() noexcept {
102 return 0;
103 }
104
106 int tail_samples() const noexcept {
107 return enabled_ && amount_ > 0.0 ? -1 : 0;
108 }
109
110 void reset() noexcept {
111 left_delay_.fill(SampleType{0});
112 right_delay_.fill(SampleType{0});
113 write_position_ = 0;
114 valid_samples_ = 0;
115 filtered_left_ = SampleType{0};
116 filtered_right_ = SampleType{0};
117 }
118
119 void process_sample(SampleType input_left, SampleType input_right, SampleType& output_left,
120 SampleType& output_right) noexcept {
121 if (!std::isfinite(static_cast<double>(input_left)) ||
122 !std::isfinite(static_cast<double>(input_right))) {
123 reset();
124 output_left = SampleType{0};
125 output_right = SampleType{0};
126 return;
127 }
128
129 left_delay_[write_position_] = input_left;
130 right_delay_[write_position_] = input_right;
131 write_position_ = (write_position_ + 1) % kDelayCapacity;
132 valid_samples_ = std::min(valid_samples_ + 1, kDelayCapacity);
133
134 const SampleType delayed_left = read_delay(left_delay_);
135 const SampleType delayed_right = read_delay(right_delay_);
136 filtered_left_ = snap_to_zero(filter_coefficient_ * filtered_left_ +
137 filter_input_coefficient_ * delayed_left);
138 filtered_right_ = snap_to_zero(filter_coefficient_ * filtered_right_ +
139 filter_input_coefficient_ * delayed_right);
140
141 if (!enabled_ || amount_ == 0.0) {
142 output_left = input_left;
143 output_right = input_right;
144 return;
145 }
146
147 output_left = direct_weight_ * input_left + cross_weight_ * filtered_right_;
148 output_right = direct_weight_ * input_right + cross_weight_ * filtered_left_;
149 if (!std::isfinite(static_cast<double>(output_left)) ||
150 !std::isfinite(static_cast<double>(output_right))) {
151 reset();
152 output_left = SampleType{0};
153 output_right = SampleType{0};
154 }
155 }
156
157 void process_block(const SampleType* input_left, const SampleType* input_right,
158 SampleType* output_left, SampleType* output_right,
159 std::size_t sample_count) noexcept {
160 if (input_left == nullptr || input_right == nullptr || output_left == nullptr ||
161 output_right == nullptr)
162 return;
163 for (std::size_t i = 0; i < sample_count; ++i) {
164 const SampleType left = input_left[i];
165 const SampleType right = input_right[i];
166 process_sample(left, right, output_left[i], output_right[i]);
167 }
168 }
169
170 private:
171 static constexpr std::size_t kDelayCapacity =
172 static_cast<std::size_t>(kMaximumSampleRate * kMaximumDelayMs / 1000.0) + 2;
173 using DelayBuffer = std::array<SampleType, kDelayCapacity>;
174
175 void update_derived_parameters() noexcept {
176 update_delay_samples();
177 update_filter();
178 update_mix();
179 }
180
181 void update_delay_samples() noexcept {
182 delay_samples_ = std::clamp(delay_ms_ * sample_rate_ / 1000.0, 0.0,
183 static_cast<double>(kDelayCapacity - 2));
184 }
185
186 void update_filter() noexcept {
187 cutoff_hz_ = std::min(requested_cutoff_hz_, 0.45 * sample_rate_);
188 filter_coefficient_ = static_cast<SampleType>(
189 std::exp(-2.0 * std::numbers::pi * cutoff_hz_ / sample_rate_));
190 filter_input_coefficient_ = SampleType{1} - filter_coefficient_;
191 }
192
193 void update_mix() noexcept {
194 const double gain = crossfeed_gain();
195 const double denominator = 1.0 + gain;
196 direct_weight_ = static_cast<SampleType>(1.0 / denominator);
197 cross_weight_ = static_cast<SampleType>(gain / denominator);
198 }
199
200 SampleType read_integer(const DelayBuffer& buffer, std::size_t delay) const noexcept {
201 if (delay >= valid_samples_)
202 return SampleType{0};
203 const std::size_t index =
204 (write_position_ + kDelayCapacity - 1 - delay) % kDelayCapacity;
205 return buffer[index];
206 }
207
208 SampleType read_delay(const DelayBuffer& buffer) const noexcept {
209 const auto younger = static_cast<std::size_t>(std::floor(delay_samples_));
210 const SampleType fraction =
211 static_cast<SampleType>(delay_samples_ - static_cast<double>(younger));
212 const SampleType a = read_integer(buffer, younger);
213 const SampleType b = read_integer(buffer, younger + 1);
214 return (SampleType{1} - fraction) * a + fraction * b;
215 }
216
217 double sample_rate_ = 48000.0;
218 double amount_ = 0.5;
219 double delay_ms_ = 0.25;
220 double delay_samples_ = 12.0;
221 double requested_cutoff_hz_ = 700.0;
222 double cutoff_hz_ = 700.0;
223 bool enabled_ = true;
224
225 SampleType filter_coefficient_{};
226 SampleType filter_input_coefficient_{};
227 SampleType direct_weight_{};
228 SampleType cross_weight_{};
229 DelayBuffer left_delay_{};
230 DelayBuffer right_delay_{};
231 std::size_t write_position_ = 0;
232 std::size_t valid_samples_ = 0;
233 SampleType filtered_left_{};
234 SampleType filtered_right_{};
235};
236
239
240} // namespace pulp::signal
Mixes a delayed, low-pass-filtered copy of each channel into the opposite ear.
Definition headphone_crossfeed.hpp:24
void set_enabled(bool enabled) noexcept
Definition headphone_crossfeed.hpp:51
static constexpr double kMinimumSampleRate
Definition headphone_crossfeed.hpp:28
static constexpr int latency_samples() noexcept
Exact zero algorithmic latency: only the opposite-channel contribution is delayed; the direct paths a...
Definition headphone_crossfeed.hpp:101
double crossfeed_gain() const noexcept
Definition headphone_crossfeed.hpp:69
void process_sample(SampleType input_left, SampleType input_right, SampleType &output_left, SampleType &output_right) noexcept
Definition headphone_crossfeed.hpp:119
double sample_rate() const noexcept
Definition headphone_crossfeed.hpp:47
HeadphoneCrossfeedT() noexcept
Definition headphone_crossfeed.hpp:33
void reset() noexcept
Definition headphone_crossfeed.hpp:110
double delay_ms() const noexcept
Definition headphone_crossfeed.hpp:80
void process_block(const SampleType *input_left, const SampleType *input_right, SampleType *output_left, SampleType *output_right, std::size_t sample_count) noexcept
Definition headphone_crossfeed.hpp:157
bool prepare(double sample_rate) noexcept
Definition headphone_crossfeed.hpp:37
void set_amount(double amount01) noexcept
Normalized crossfeed amount.
Definition headphone_crossfeed.hpp:60
static constexpr double kMaximumCrossfeedGain
Definition headphone_crossfeed.hpp:31
double cutoff_hz() const noexcept
Definition headphone_crossfeed.hpp:95
int tail_samples() const noexcept
The one-pole feed decays asymptotically whenever crossfeed is active.
Definition headphone_crossfeed.hpp:106
static constexpr double kMaximumSampleRate
Definition headphone_crossfeed.hpp:29
double delay_samples() const noexcept
Definition headphone_crossfeed.hpp:83
void set_cutoff_hz(double cutoff_hz) noexcept
Crossfeed low-pass cutoff.
Definition headphone_crossfeed.hpp:89
double amount() const noexcept
Definition headphone_crossfeed.hpp:66
void set_delay_ms(double delay_ms) noexcept
Interaural delay in milliseconds, clamped to [0, 1].
Definition headphone_crossfeed.hpp:74
static constexpr double kMaximumDelayMs
Definition headphone_crossfeed.hpp:30
bool enabled() const noexcept
Definition headphone_crossfeed.hpp:54
@ b
Definition pitch.hpp:31
@ index
Definition gpu_pass.hpp:140
Definition additive_bank.hpp:218
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
HeadphoneCrossfeedT< float > HeadphoneCrossfeed
Definition headphone_crossfeed.hpp:237
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ delay
Definition envelope.hpp:57
HeadphoneCrossfeedT< double > HeadphoneCrossfeed64
Definition headphone_crossfeed.hpp:238
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20