29 static_assert(std::is_floating_point_v<SampleType>);
41 if (!std::isfinite(sample_rate) || sample_rate < 20.0 / 0.45 ||
capacity < 1 ||
45 auto candidate = std::make_unique<PreparedState>();
46 candidate->sample_rate = sample_rate;
48 const double initial_frequency = std::min(1000.0, 0.45 * sample_rate);
50 auto& band = candidate->bands[
static_cast<std::size_t
>(
i)];
51 band.current.frequency_hz = initial_frequency;
52 band.target.frequency_hz = initial_frequency;
53 band.configured_frequency_hz = initial_frequency;
54 band.filter.configure(Svf64::Mode::bandpass, sample_rate, initial_frequency, 4.0);
55 band.envelope.prepare(sample_rate);
56 band.envelope.set_attack_ms(5.0);
57 band.envelope.set_release_ms(50.0);
59 state_ = std::move(candidate);
65 [[nodiscard]]
bool stage_band(
int index,
const BandRecipe& recipe)
noexcept {
66 if (!state_ || index < 0 || index >= state_->capacity)
68 staged_[
static_cast<std::size_t
>(index)] = recipe;
73 if (!valid_transition(transition_ms))
75 transition_ms_ = transition_ms;
79 [[nodiscard]]
bool publish(
int active_count)
noexcept {
80 return publish(active_count, transition_ms_);
83 [[nodiscard]]
bool publish(
int active_count,
double transition_ms)
noexcept {
84 if (!state_ || active_count < 0 || active_count > state_->capacity ||
85 !valid_transition(transition_ms))
87 for (
int i = 0;
i < active_count; ++
i)
88 if (!valid_recipe(staged_[
static_cast<std::size_t
>(
i)], state_->sample_rate))
90 const double transition_samples_exact = transition_ms * state_->sample_rate / 1000.0;
91 if (!std::isfinite(transition_samples_exact) ||
92 transition_samples_exact >=
93 static_cast<double>(std::numeric_limits<std::uint64_t>::max()))
96 PublishedRecipe payload;
97 payload.bands = staged_;
98 payload.active_count = active_count;
99 payload.transition_samples =
100 static_cast<std::uint64_t
>(std::ceil(transition_samples_exact));
101 payload.generation = next_generation_++;
102 if (next_generation_ == 0)
104 state_->publication.write(payload);
108 SampleType
process(SampleType input)
noexcept {
112 if (!std::isfinite(
static_cast<double>(input))) {
113 reset_active_audio_state();
118 for (
int i = 0;
i < state_->active_count; ++
i) {
119 auto& band = state_->bands[
static_cast<std::size_t
>(
i)];
121 if (band.configured_attack_ms != band.current.attack_ms) {
122 band.envelope.set_attack_ms(band.current.attack_ms);
123 band.configured_attack_ms = band.current.attack_ms;
125 if (band.configured_release_ms != band.current.release_ms) {
126 band.envelope.set_release_ms(band.current.release_ms);
127 band.configured_release_ms = band.current.release_ms;
129 if (band.configured_frequency_hz != band.current.frequency_hz ||
130 band.configured_q != band.current.q) {
131 band.filter.configure(Svf64::Mode::bandpass, state_->sample_rate,
132 band.current.frequency_hz, band.current.q);
133 band.configured_frequency_hz = band.current.frequency_hz;
134 band.configured_q = band.current.q;
136 const double filtered = band.filter.process(
static_cast<double>(input));
137 const double level = band.envelope.process(std::abs(filtered));
138 const double output = filtered * db_to_gain(band.current.gain_db);
139 band.last_output = std::isfinite(filtered) ? filtered : 0.0;
140 band.last_envelope = std::isfinite(level) ? level : 0.0;
141 if (!std::isfinite(output) || !std::isfinite(level)) {
143 band.envelope.reset();
145 sum += std::isfinite(output) ? output : 0.0;
147 if (!std::isfinite(sum))
149 return to_sample(sum);
152 void process(
const SampleType* input, SampleType* output,
int num_samples)
noexcept {
153 if (!output || num_samples <= 0)
155 for (
int i = 0;
i < num_samples; ++
i)
156 output[
i] =
process(input ? input[
i] : SampleType{});
163 reset_active_audio_state();
164 for (
int i = 0;
i < state_->active_count; ++
i) {
165 auto& band = state_->bands[
static_cast<std::size_t
>(
i)];
166 band.current = band.target;
172 return state_ ? state_->capacity : 0;
175 return state_ ? state_->active_count : 0;
181 return state_ ?
sizeof(PreparedState) : 0;
185 return valid_active_index(index)
186 ? to_sample(state_->bands[
static_cast<std::size_t
>(index)].last_output)
190 return valid_active_index(index)
191 ? to_sample(state_->bands[
static_cast<std::size_t
>(index)].last_envelope)
196 return state_ && index >= 0 && index < state_->capacity
197 ? state_->bands[
static_cast<std::size_t
>(index)].current
202 return valid_tail_recipe(recipe)
203 ? 6.907755278982137 * recipe.q / (3.14159265358979323846 * recipe.frequency_hz)
208 struct PublishedRecipe {
209 std::array<BandRecipe, kMaximumBands> bands{};
210 int active_count = 0;
211 std::uint64_t transition_samples = 0;
212 std::uint64_t generation = 0;
216 BallisticsFilterT<double> envelope;
217 BandRecipe current{};
220 std::uint64_t remaining = 0;
221 double last_output = 0.0;
222 double last_envelope = 0.0;
223 double configured_attack_ms = 5.0;
224 double configured_release_ms = 50.0;
225 double configured_frequency_hz = 1000.0;
226 double configured_q = 4.0;
228 struct PreparedState {
229 double sample_rate = 0.0;
231 int active_count = 0;
232 std::uint64_t consumed_generation = 0;
233 runtime::TripleBuffer<PublishedRecipe> publication;
234 std::array<BandState, kMaximumBands> bands{};
237 static bool valid_recipe(
const BandRecipe& r,
double sample_rate)
noexcept {
238 return std::isfinite(
r.frequency_hz) &&
r.frequency_hz >= 20.0 &&
239 r.frequency_hz <= 0.45 * sample_rate && std::isfinite(
r.q) &&
r.q >= 0.5 &&
240 r.q <= 100.0 && std::isfinite(
r.gain_db) &&
r.gain_db >= -60.0 &&
241 r.gain_db <= 24.0 && std::isfinite(
r.attack_ms) &&
r.attack_ms >= 0.01 &&
242 r.attack_ms <= 60000.0 && std::isfinite(
r.release_ms) &&
r.release_ms >= 0.01 &&
243 r.release_ms <= 60000.0;
245 static bool valid_transition(
double ms)
noexcept {
246 return std::isfinite(ms) && ms >= 0.0 && ms <= 500.0;
248 static bool valid_tail_recipe(
const BandRecipe& recipe)
noexcept {
249 return std::isfinite(recipe.frequency_hz) && recipe.frequency_hz > 0.0 &&
250 std::isfinite(recipe.q) && recipe.q > 0.0;
252 static double db_to_gain(
double db)
noexcept {
253 return std::pow(10.0, db / 20.0);
255 static SampleType to_sample(
double value)
noexcept {
256 if (!std::isfinite(value))
258 const double limit =
static_cast<double>(std::numeric_limits<SampleType>::max());
259 return static_cast<SampleType
>(std::clamp(value, -limit, limit));
262 return {(
b.frequency_hz -
a.frequency_hz) / n, (
b.q -
a.q) /
n, (
b.gain_db -
a.gain_db) / n,
263 (
b.attack_ms -
a.attack_ms) /
n, (
b.release_ms -
a.release_ms) / n};
266 a.frequency_hz +=
d.frequency_hz;
268 a.gain_db +=
d.gain_db;
269 a.attack_ms +=
d.attack_ms;
270 a.release_ms +=
d.release_ms;
272 void adopt_publication() noexcept {
273 const auto&
p = state_->publication.read();
274 if (
p.generation == 0 ||
p.generation == state_->consumed_generation)
276 const int touched_bands = std::max(state_->active_count,
p.active_count);
277 for (
int i = 0;
i < touched_bands; ++
i) {
278 auto& band = state_->bands[
static_cast<std::size_t
>(
i)];
279 if (
i >=
p.active_count) {
281 band.envelope.reset();
282 band.last_output = 0.0;
283 band.last_envelope = 0.0;
287 band.target =
p.bands[
static_cast<std::size_t
>(
i)];
288 if (
p.transition_samples == 0) {
289 band.current = band.target;
293 delta(band.current, band.target,
static_cast<double>(
p.transition_samples));
294 band.remaining =
p.transition_samples;
297 state_->active_count =
p.active_count;
298 state_->consumed_generation =
p.generation;
300 static void advance(BandState& band)
noexcept {
301 if (band.remaining == 0)
303 add(band.current, band.step);
304 if (--band.remaining == 0)
305 band.current = band.target;
307 void reset_active_audio_state() noexcept {
308 for (
int i = 0;
i < state_->active_count; ++
i) {
309 auto& band = state_->bands[
static_cast<std::size_t
>(
i)];
311 band.envelope.reset();
312 band.last_output = 0.0;
313 band.last_envelope = 0.0;
316 bool valid_active_index(
int index)
const noexcept {
317 return state_ &&
index >= 0 &&
index < state_->active_count;
320 std::unique_ptr<PreparedState> state_;
321 std::array<BandRecipe, kMaximumBands> staged_{};
322 double transition_ms_ = 20.0;
323 std::uint64_t next_generation_ = 1;
Peak/RMS envelope follower with independent attack and release curves.
Definition explicit_q_resonator_bank.hpp:27
static constexpr int kMaximumBands
Definition explicit_q_resonator_bank.hpp:30
std::size_t retained_bytes() const noexcept
Definition explicit_q_resonator_bank.hpp:180
BandRecipe applied_band_recipe(int index) const noexcept
Current RT-side recipe, including any in-flight transition.
Definition explicit_q_resonator_bank.hpp:195
void process(const SampleType *input, SampleType *output, int num_samples) noexcept
Definition explicit_q_resonator_bank.hpp:152
SampleType process(SampleType input) noexcept
Definition explicit_q_resonator_bank.hpp:108
bool stage_band(int index, const BandRecipe &recipe) noexcept
Definition explicit_q_resonator_bank.hpp:65
int active_band_count() const noexcept
Definition explicit_q_resonator_bank.hpp:174
int capacity() const noexcept
Definition explicit_q_resonator_bank.hpp:171
bool set_coefficient_transition_ms(double transition_ms) noexcept
Definition explicit_q_resonator_bank.hpp:72
bool prepare(double sample_rate, int capacity)
Definition explicit_q_resonator_bank.hpp:40
bool publish(int active_count, double transition_ms) noexcept
Definition explicit_q_resonator_bank.hpp:83
int latency_samples() const noexcept
Definition explicit_q_resonator_bank.hpp:177
SampleType envelope_at(int index) const noexcept
Definition explicit_q_resonator_bank.hpp:189
static double estimated_t60_seconds(const BandRecipe &recipe) noexcept
Ideal second-order -60 dB amplitude-tail estimate used for render sizing.
Definition explicit_q_resonator_bank.hpp:201
void reset() noexcept
Definition explicit_q_resonator_bank.hpp:159
bool publish(int active_count) noexcept
Definition explicit_q_resonator_bank.hpp:79
SampleType band_output(int index) const noexcept
Last filtered sample before gain_db is applied to the summed output.
Definition explicit_q_resonator_bank.hpp:184
@ d
Definition pitch.hpp:22
@ b
Definition pitch.hpp:31
@ index
Definition gpu_pass.hpp:140
Definition additive_bank.hpp:218
ExplicitQResonatorBankT< double > ExplicitQResonatorBank64
Definition explicit_q_resonator_bank.hpp:327
ExplicitQResonatorBankT< float > ExplicitQResonatorBank
Definition explicit_q_resonator_bank.hpp:326
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
SvfT< double > Svf64
Definition svf.hpp:149
@ r
Definition input_events.hpp:41
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
Definition explicit_q_resonator_bank.hpp:32
double q
Definition explicit_q_resonator_bank.hpp:34
double gain_db
Definition explicit_q_resonator_bank.hpp:35
double release_ms
Definition explicit_q_resonator_bank.hpp:37
double frequency_hz
Definition explicit_q_resonator_bank.hpp:33
double attack_ms
Definition explicit_q_resonator_bank.hpp:36
Lock-free latest-value publication between two threads.