MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
FeedbackBuffer.cpp
Go to the documentation of this file.
1#include "FeedbackBuffer.hpp"
2
5
7
8namespace MayaFlux::Buffers {
9
10//-----------------------------------------------------------------------------
11// FeedbackBuffer
12//-----------------------------------------------------------------------------
13
14FeedbackBuffer::FeedbackBuffer(uint32_t channel_id, uint32_t num_samples,
15 float feedback, uint32_t feed_samples)
16 : AudioBuffer(channel_id, num_samples)
17 , m_feedback_amount(feedback)
18 , m_feed_samples(std::max<uint32_t>(feed_samples, 1))
19 , m_history(m_feed_samples)
20{
22}
23
25{
26 m_default_processor->process(shared_from_this());
27}
28
30{
31 m_feedback_amount = amount;
32 if (auto proc = std::dynamic_pointer_cast<FeedbackProcessor>(m_default_processor)) {
33 proc->set_feedback(amount);
34 }
35}
36
38{
39 m_feed_samples = std::max<uint32_t>(samples, 1);
41 if (auto proc = std::dynamic_pointer_cast<FeedbackProcessor>(m_default_processor)) {
42 proc->set_feed_samples(m_feed_samples);
43 }
44}
45
46std::shared_ptr<BufferProcessor> FeedbackBuffer::create_default_processor()
47{
48 return std::make_shared<FeedbackProcessor>(m_feedback_amount, m_feed_samples);
49}
50
51std::shared_ptr<AudioBuffer> FeedbackBuffer::clone_to(uint32_t channel)
52{
53 auto clone = std::make_shared<FeedbackBuffer>(channel, get_num_samples(), m_feedback_amount, m_feed_samples);
54 clone->get_data() = get_data();
55
56 if (auto proc = std::dynamic_pointer_cast<FeedbackProcessor>(m_default_processor)) {
57 clone->set_default_processor(proc->clone());
58 }
59
60 clone->set_processing_chain(get_processing_chain(), true);
61 return clone;
62}
63
64//-----------------------------------------------------------------------------
65// FeedbackProcessor
66//-----------------------------------------------------------------------------
67
68FeedbackProcessor::FeedbackProcessor(float feedback, uint32_t feed_samples)
69 : m_feedback_amount(feedback)
70 , m_feed_samples(std::max<uint32_t>(feed_samples, 1))
71 , m_default_combine(true)
72{
73 m_combine = [this](double x, std::span<const double> taps, std::span<double>) {
74 return x + static_cast<double>(m_feedback_amount) * taps[0];
75 };
76
77 Tap tap;
78 tap.samples = static_cast<double>(m_feed_samples);
79 tap.max_samples = tap.samples;
80 m_taps.push_back(std::move(tap));
82}
83
84FeedbackProcessor::FeedbackProcessor(Combine combine, const std::vector<double>& lags, std::vector<double> coefficients)
85 : m_combine(std::move(combine))
86 , m_coefficients(std::move(coefficients))
87{
88 set_lags(lags);
89}
90
91FeedbackProcessor::FeedbackProcessor(CombineStep combine, const std::vector<double>& lags, std::vector<double> coefficients)
92 : m_step(std::move(combine))
93 , m_coefficients(std::move(coefficients))
94{
95 set_lags(lags);
96}
97
98FeedbackProcessor::FeedbackProcessor(WindowKernel kernel, const std::vector<double>& lags, std::vector<double> coefficients)
99 : m_kernel(std::move(kernel))
100 , m_coefficients(std::move(coefficients))
101{
102 set_lags(lags);
103}
104
106{
107 m_feed_samples = std::max<uint32_t>(samples, 1);
108 if (m_taps.empty()) {
109 m_taps.emplace_back();
110 }
111 m_taps.front().samples = static_cast<double>(m_feed_samples);
112 m_taps.front().map.reset();
113 m_taps.front().max_samples = m_taps.front().samples;
115}
116
118{
119 m_combine = std::move(combine);
120 m_step = nullptr;
121 m_kernel = nullptr;
122 m_default_combine = false;
123}
124
126{
127 m_step = std::move(combine);
128 m_combine = nullptr;
129 m_kernel = nullptr;
130 m_default_combine = false;
131}
132
134{
135 m_kernel = std::move(kernel);
136 m_combine = nullptr;
137 m_step = nullptr;
138 m_default_combine = false;
139}
140
142{
143 m_order = std::clamp<size_t>(order, 1, 7);
145}
146
147void FeedbackProcessor::set_lag_source(const std::shared_ptr<FeedbackBuffer>& source)
148{
149 m_lag_source = source;
150}
151
156
157void FeedbackProcessor::set_lags(const std::vector<double>& lags)
158{
159 m_taps.clear();
160 m_taps.reserve(lags.size());
161 for (const double lag : lags) {
162 Tap tap;
163 tap.samples = std::max(lag, 1.0);
164 tap.max_samples = tap.samples;
165 m_taps.push_back(std::move(tap));
166 }
167
168 if (!m_taps.empty()) {
169 m_feed_samples = static_cast<uint32_t>(std::max(std::round(m_taps.front().samples), 1.0));
170 }
172}
173
174void FeedbackProcessor::set_lag(size_t tap, Kinesis::TimeMap seconds, double max_seconds)
175{
176 if (tap >= m_taps.size()) {
177 error<std::out_of_range>(Journal::Component::Buffers, Journal::Context::Configuration, std::source_location::current(),
178 "FeedbackProcessor::set_lag: tap out of range (tap={}, tap_count={})", tap, m_taps.size());
179 }
180
181 m_taps[tap].map = std::make_shared<const Kinesis::TimeMap>(std::move(seconds));
182 m_taps[tap].max_samples = std::max(1.0, max_seconds * static_cast<double>(s_registered_sample_rate));
184}
185
186void FeedbackProcessor::set_coefficients(std::vector<double> coefficients)
187{
188 m_coefficients = std::move(coefficients);
189 for (auto& [buffer, state] : m_states) {
190 state.coefs = m_coefficients;
191 }
192}
193
194std::span<double> FeedbackProcessor::coefficients(const std::shared_ptr<Buffer>& buffer)
195{
196 const auto found = m_states.find(buffer.get());
197 if (found == m_states.end()) {
198 return {};
199 }
200 return found->second.coefs;
201}
202
203std::shared_ptr<FeedbackProcessor> FeedbackProcessor::clone() const
204{
205 auto copy = std::make_shared<FeedbackProcessor>(m_feedback_amount, m_feed_samples);
206
207 copy->m_taps = m_taps;
208 copy->m_coefficients = m_coefficients;
209 copy->m_order = m_order;
210 copy->m_lag_source = m_lag_source;
211 copy->m_capacity = m_capacity;
212
213 if (!m_default_combine) {
214 copy->m_default_combine = false;
215 copy->m_combine = m_combine;
216 copy->m_step = m_step;
217 copy->m_kernel = m_kernel;
218 }
219
220 return copy;
221}
222
224{
225 double needed = 1.0;
226 for (const auto& tap : m_taps) {
227 needed = std::max(needed, tap.max_samples);
228 }
229 const size_t extra = m_order > 1 ? m_order : 0;
230 m_capacity = static_cast<size_t>(std::ceil(needed)) + extra;
231}
232
234{
235 auto [found, inserted] = m_states.try_emplace(buffer.get());
236 State& state = found->second;
237
238 if (inserted) {
239 state.coefs = m_coefficients;
240 state.values.assign(m_taps.size(), 0.0);
241 state.window.assign(m_taps.size() + 1, 0.0);
242 state.lag_prev.assign(m_taps.size(), 1.0);
243 state.lag_now.assign(m_taps.size(), 1.0);
244
245 if (auto feedback_buffer = std::dynamic_pointer_cast<FeedbackBuffer>(buffer)) {
246 state.ring = &feedback_buffer->get_history_buffer();
247 } else {
248 state.own.resize(m_capacity);
249 state.ring = &state.own;
250 }
251 }
252
253 return state;
254}
255
256void FeedbackProcessor::on_attach(const std::shared_ptr<Buffer>& buffer)
257{
259}
260
261void FeedbackProcessor::on_detach(const std::shared_ptr<Buffer>& buffer)
262{
263 m_states.erase(buffer.get());
264}
265
266bool FeedbackProcessor::is_compatible_with(const std::shared_ptr<Buffer>& buffer) const
267{
268 return std::dynamic_pointer_cast<AudioBuffer>(buffer) != nullptr;
269}
270
272{
273 const auto whole = static_cast<size_t>(lag);
274 const double fraction = lag - static_cast<double>(whole);
275 const double nearer = ring[whole - 1];
276
277 if (fraction <= 0.0) {
278 return nearer;
279 }
280 return nearer + (ring[whole] - nearer) * fraction;
281}
282
283double FeedbackProcessor::read_at(const Memory::HistoryBuffer<double>& ring, double position, size_t order) noexcept
284{
285 const size_t capacity = ring.capacity();
286 const double clamped = std::clamp(position, 0.0, static_cast<double>(capacity - 1));
287
288 if (order <= 1) {
289 const auto whole = static_cast<size_t>(clamped);
290 const double fraction = clamped - static_cast<double>(whole);
291 const double nearer = ring[whole];
292 if (fraction <= 0.0 || whole + 1 >= capacity) {
293 return nearer;
294 }
295 return nearer + (ring[whole + 1] - nearer) * fraction;
296 }
297
298 const size_t taps = std::min(order + 1, capacity);
299 std::array<double, 8> weights {};
300
301 const auto centre = static_cast<size_t>(clamped);
302 const size_t before = (taps - 1) / 2;
303 const size_t first = std::min(centre > before ? centre - before : 0, capacity - taps);
304
305 Kinesis::Discrete::lagrange_weights(clamped - static_cast<double>(first), std::span<double>(weights.data(), taps));
306
307 double sum = 0.0;
308 for (size_t k = 0; k < taps; ++k) {
309 sum += weights[k] * ring[first + k];
310 }
311 return sum;
312}
313
314void FeedbackProcessor::processing_function(const std::shared_ptr<Buffer>& buffer)
315{
316 auto audio = std::dynamic_pointer_cast<AudioBuffer>(buffer);
317 if (!audio) {
318 return;
319 }
320
321 const bool use_step = static_cast<bool>(m_step);
322 const bool use_kernel = !use_step && !m_combine && static_cast<bool>(m_kernel);
323 if (!use_step && !use_kernel && !m_combine) {
324 return;
325 }
326
327 auto& data = audio->get_data();
328 const size_t samples = data.size();
329 const size_t taps = m_taps.size();
330 if (samples == 0 || taps == 0) {
331 return;
332 }
333
334 State& state = state_for(buffer);
335 auto& ring = *state.ring;
336
337 if (ring.capacity() < m_capacity) {
338 ring.resize(m_capacity);
339 }
340
341 if (state.values.size() != taps) {
342 state.values.assign(taps, 0.0);
343 state.window.assign(taps + 1, 0.0);
344 state.lag_prev.assign(taps, 1.0);
345 state.lag_now.assign(taps, 1.0);
346 state.primed = false;
347 }
348
349 const auto source = m_lag_source.lock();
350 const bool foreign = source && static_cast<const Buffer*>(source.get()) != buffer.get();
351
352 const Memory::HistoryBuffer<double>* read_ring = &ring;
353 double offset = 0.0;
354 if (foreign) {
355 auto& source_ring = source->get_history_buffer();
356 if (source_ring.capacity() < m_capacity + samples) {
357 source_ring.resize(m_capacity + samples);
358 }
359 read_ring = &source_ring;
360 offset = static_cast<double>(source->get_write_count()) - static_cast<double>(state.elapsed);
361 }
362
363 const size_t extra = m_order > 1 ? m_order : 0;
364 const auto rate = static_cast<double>(s_registered_sample_rate);
365 const auto seconds = static_cast<double>(state.elapsed) / rate;
366 const double lowest = foreign ? std::max(1.0, static_cast<double>(samples)) : 1.0;
367 const double highest = std::max(static_cast<double>((foreign ? m_capacity : ring.capacity()) - extra), lowest);
368
369 for (size_t t = 0; t < taps; ++t) {
370 const double raw = m_taps[t].map ? (*m_taps[t].map)(seconds) * rate : m_taps[t].samples;
371 state.lag_now[t] = std::clamp(raw, lowest, highest);
372 }
373 if (!state.primed) {
374 state.lag_prev = state.lag_now;
375 state.primed = true;
376 }
377
378 const double inverse = 1.0 / static_cast<double>(samples);
379 const bool plain_read = !foreign && m_order <= 1;
380
381 for (size_t i = 0; i < samples; ++i) {
382 const double ramp = static_cast<double>(i + 1) * inverse;
383 const double shift = foreign ? offset - static_cast<double>(i) : 0.0;
384
385 for (size_t t = 0; t < taps; ++t) {
386 const double lag = state.lag_prev[t] + (state.lag_now[t] - state.lag_prev[t]) * ramp;
387 state.values[t] = plain_read ? read(ring, lag) : read_at(*read_ring, lag - 1.0 + shift, m_order);
388 }
389
390 const std::span<const double> tap_values(state.values);
391 const std::span<double> coefs(state.coefs);
392
393 double out {};
394 double carry {};
395 if (use_step) {
396 const auto [step_out, step_carry] = m_step(data[i], tap_values, coefs);
397 out = step_out;
398 carry = step_carry;
399 } else if (use_kernel) {
400 state.window[0] = data[i];
401 std::copy(state.values.begin(), state.values.end(), state.window.begin() + 1);
402 out = m_kernel(std::span<double>(state.window), coefs);
403 carry = out;
404 } else {
405 out = m_combine(data[i], tap_values, coefs);
406 carry = out;
407 }
408
409 data[i] = out;
410 ring.push(carry);
411 }
412
413 state.lag_prev = state.lag_now;
414 state.elapsed += samples;
415
416 if (auto owner = std::dynamic_pointer_cast<FeedbackBuffer>(buffer)) {
417 owner->mark_written(samples);
418 }
419}
420
421} // namespace MayaFlux::Buffers
Digital filter coefficient design for MayaFlux::Kinesis.
uint64_t offset
Definition Composite.cpp:10
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
float rate
std::shared_ptr< NetworkGeometryBuffer > buffer
glm::ivec3 max
std::vector< float > * out
virtual std::vector< double > & get_data()
Gets mutable access to the buffer's underlying audio data.
virtual uint32_t get_num_samples() const
Gets the current capacity of the audio buffer.
std::shared_ptr< BufferProcessor > m_default_processor
Default audio transformation processor for this buffer.
std::shared_ptr< BufferProcessingChain > get_processing_chain() override
Gets the audio transformation chain attached to this buffer.
Concrete audio implementation of the Buffer interface for double-precision audio data.
Backend-agnostic interface for sequential data storage and transformation.
Definition Buffer.hpp:37
void process_default() override
Applies the default audio transformation to the buffer's data.
FeedbackBuffer(uint32_t channel_id, uint32_t num_samples, float feedback=0.5F, uint32_t feed_samples=512)
Memory::HistoryBuffer< double > m_history
std::shared_ptr< BufferProcessor > create_default_processor() override
Creates a default audio transformation processor for this buffer type.
void set_feed_samples(uint32_t samples)
std::shared_ptr< AudioBuffer > clone_to(uint32_t channel) override
Clone for another channel with its own ring and its own processor state.
FeedbackProcessor(float feedback=0.5F, uint32_t feed_samples=512)
Single tap, output = input + feedback * tap.
std::function< Step(double, std::span< const double >, std::span< double >)> CombineStep
Combiner returning an output and a carried value.
std::shared_ptr< FeedbackProcessor > clone() const
Same configuration, no attached state.
void on_detach(const std::shared_ptr< Buffer > &buffer) override
Called when this processor is detached from a buffer.
void set_lag_source(const std::shared_ptr< FeedbackBuffer > &source)
Read the taps from another FeedbackBuffer's ring.
std::weak_ptr< FeedbackBuffer > m_lag_source
static double read_at(const Memory::HistoryBuffer< double > &ring, double position, size_t order) noexcept
std::unordered_map< const Buffer *, State > m_states
void set_lag(size_t tap, Kinesis::TimeMap seconds, double max_seconds)
Drive one tap's lag from a TimeMap.
void set_interpolation(size_t order)
Interpolation used for fractional lags.
void processing_function(const std::shared_ptr< Buffer > &buffer) override
The core processing function that must be implemented by derived classes.
void set_feed_samples(uint32_t samples)
Lag of the first tap, in samples.
State & state_for(const std::shared_ptr< Buffer > &buffer)
std::function< double(double, std::span< const double >, std::span< double >)> Combine
Combiner returning one value, written to the buffer and carried.
std::span< double > coefficients(const std::shared_ptr< Buffer > &buffer)
The coefficient array of one attached buffer, empty if it is not attached.
void set_lags(const std::vector< double > &lags)
Replace all taps with fixed lags in samples.
void set_coefficients(std::vector< double > coefficients)
Set the initial coefficient array and reset every attached buffer's copy.
void on_attach(const std::shared_ptr< Buffer > &buffer) override
Called when this processor is attached to a buffer.
bool is_compatible_with(const std::shared_ptr< Buffer > &buffer) const override
Checks if this processor can handle the specified buffer type.
static double read(const Memory::HistoryBuffer< double > &ring, double lag)
std::function< double(std::span< double >, std::span< double >)> WindowKernel
A function of one window and one array, as in Kinesis::Discrete::Kernels.
void resize(size_t new_capacity)
Resize buffer capacity.
size_t capacity() const
Get buffer capacity.
History buffer for difference equations and recursive relations.
uint64_t s_registered_sample_rate
Global default sample rate.
@ Configuration
Configuration and parameter updates.
@ Buffers
Buffers, Managers, processors and processing chains.
void lagrange_weights(double delay, std::span< double > weights) noexcept
Lagrange interpolation weights written to an array, without allocating.
Memory::HistoryBuffer< double > * ring
Typed, composable, stateless callable from domain D to range R.
Definition Tendency.hpp:22