filter-graph: add feedback and feedforward to delay

Add feedback and feedforward controls to the delay. This makes it
possible to make comb and allpass filters with the delay to build
custom reverb effects.
This commit is contained in:
Wim Taymans 2025-11-28 17:10:55 +01:00
parent 933ac4be43
commit 52ec847cbd
6 changed files with 104 additions and 37 deletions

View file

@ -205,9 +205,10 @@ void dsp_linear_c(void *obj, float * dst,
void dsp_delay_c(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer,
uint32_t delay, float *dst, const float *src, uint32_t n_samples)
uint32_t delay, float *dst, const float *src, uint32_t n_samples,
float fb, float ff)
{
if (delay == 0) {
if (delay == 0 && fb == 0.0f && ff == 0.0f) {
dsp_copy_c(obj, dst, src, n_samples);
} else {
uint32_t w, o, i;
@ -215,10 +216,20 @@ void dsp_delay_c(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer,
w = *pos;
o = n_buffer - delay;
for (i = 0; i < n_samples; i++) {
buffer[w] = buffer[w + n_buffer] = src[i];
dst[i] = buffer[w + o];
w = w + 1 >= n_buffer ? 0 : w + 1;
if (fb == 0.0f && ff == 0.0f) {
for (i = 0; i < n_samples; i++) {
buffer[w] = buffer[w + n_buffer] = src[i];
dst[i] = buffer[w + o];
w = w + 1 >= n_buffer ? 0 : w + 1;
}
} else {
for (i = 0; i < n_samples; i++) {
float d = buffer[w + o];
float s = src[i];
buffer[w] = buffer[w + n_buffer] = s + d * fb;
dst[i] = ff * s + d;
w = w + 1 >= n_buffer ? 0 : w + 1;
}
}
*pos = w;
}

View file

@ -32,7 +32,7 @@ void dsp_biquad_run_##arch (void *obj, struct biquad *bq, uint32_t n_bq, uint32_
float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], uint32_t n_src, uint32_t n_samples)
#define MAKE_DELAY_FUNC(arch) \
void dsp_delay_##arch (void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, \
uint32_t delay, float *dst, const float *src, uint32_t n_samples)
uint32_t delay, float *dst, const float *src, uint32_t n_samples, float fb, float ff)
#define MAKE_FFT_NEW_FUNC(arch) \
void *dsp_fft_new_##arch(void *obj, uint32_t size, bool real)

View file

@ -614,34 +614,70 @@ void dsp_biquad_run_sse(void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq
}
void dsp_delay_sse(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay,
float *dst, const float *src, uint32_t n_samples)
float *dst, const float *src, uint32_t n_samples, float fb, float ff)
{
__m128 t[1];
__m128 t[4];
uint32_t w = *pos;
uint32_t o = n_buffer - delay;
uint32_t n, unrolled;
if (SPA_IS_ALIGNED(src, 16) &&
SPA_IS_ALIGNED(dst, 16))
unrolled = n_samples & ~3;
else
unrolled = 0;
if (fb == 0.0f && ff == 0.0f) {
if (SPA_IS_ALIGNED(src, 16) &&
SPA_IS_ALIGNED(dst, 16) && delay >= 4)
unrolled = n_samples & ~3;
else
unrolled = 0;
for(n = 0; n < unrolled; n += 4) {
t[0] = _mm_load_ps(&src[n]);
_mm_storeu_ps(&buffer[w], t[0]);
_mm_storeu_ps(&buffer[w+n_buffer], t[0]);
t[0] = _mm_loadu_ps(&buffer[w+o]);
_mm_store_ps(&dst[n], t[0]);
w = w + 4 >= n_buffer ? 0 : w + 4;
}
for(; n < n_samples; n++) {
t[0] = _mm_load_ss(&src[n]);
_mm_store_ss(&buffer[w], t[0]);
_mm_store_ss(&buffer[w+n_buffer], t[0]);
t[0] = _mm_load_ss(&buffer[w+o]);
_mm_store_ss(&dst[n], t[0]);
w = w + 1 >= n_buffer ? 0 : w + 1;
for(n = 0; n < unrolled; n += 4) {
t[0] = _mm_load_ps(&src[n]);
_mm_storeu_ps(&buffer[w], t[0]);
_mm_storeu_ps(&buffer[w+n_buffer], t[0]);
t[0] = _mm_loadu_ps(&buffer[w+o]);
_mm_store_ps(&dst[n], t[0]);
w = w + 4 >= n_buffer ? 0 : w + 4;
}
for(; n < n_samples; n++) {
t[0] = _mm_load_ss(&src[n]);
_mm_store_ss(&buffer[w], t[0]);
_mm_store_ss(&buffer[w+n_buffer], t[0]);
t[0] = _mm_load_ss(&buffer[w+o]);
_mm_store_ss(&dst[n], t[0]);
w = w + 1 >= n_buffer ? 0 : w + 1;
}
} else {
__m128 fb0 = _mm_set1_ps(fb);
__m128 ff0 = _mm_set1_ps(ff);
if (SPA_IS_ALIGNED(src, 16) &&
SPA_IS_ALIGNED(dst, 16) && delay >= 4)
unrolled = n_samples & ~3;
else
unrolled = 0;
for(n = 0; n < unrolled; n += 4) {
t[0] = _mm_loadu_ps(&buffer[w+o]);
t[1] = _mm_load_ps(&src[n]);
t[2] = _mm_mul_ps(t[0], fb0);
t[2] = _mm_add_ps(t[2], t[1]);
_mm_storeu_ps(&buffer[w], t[2]);
_mm_storeu_ps(&buffer[w+n_buffer], t[2]);
t[2] = _mm_mul_ps(t[1], ff0);
t[2] = _mm_add_ps(t[2], t[0]);
_mm_store_ps(&dst[n], t[2]);
w = w + 4 >= n_buffer ? 0 : w + 4;
}
for(; n < n_samples; n++) {
t[0] = _mm_load_ss(&buffer[w+o]);
t[1] = _mm_load_ss(&src[n]);
t[2] = _mm_mul_ss(t[0], fb0);
t[2] = _mm_add_ss(t[2], t[1]);
_mm_store_ss(&buffer[w], t[2]);
_mm_store_ss(&buffer[w+n_buffer], t[2]);
t[2] = _mm_mul_ps(t[1], ff0);
t[2] = _mm_add_ps(t[2], t[0]);
_mm_store_ss(&dst[n], t[2]);
w = w + 1 >= n_buffer ? 0 : w + 1;
}
}
*pos = w;
}

View file

@ -58,7 +58,8 @@ struct spa_fga_dsp_methods {
float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[],
uint32_t n_src, uint32_t n_samples);
void (*delay) (void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay,
float *dst, const float *src, uint32_t n_samples);
float *dst, const float *src, uint32_t n_samples,
float fb, float ff);
};
static inline void spa_fga_dsp_clear(struct spa_fga_dsp *obj, float * SPA_RESTRICT dst, uint32_t n_samples)
@ -159,10 +160,11 @@ static inline void spa_fga_dsp_biquad_run(struct spa_fga_dsp *obj,
}
static inline void spa_fga_dsp_delay(struct spa_fga_dsp *obj,
float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay,
float *dst, const float *src, uint32_t n_samples)
float *dst, const float *src, uint32_t n_samples,
float fb, float ff)
{
spa_api_method_v(spa_fga_dsp, &obj->iface, delay, 0,
buffer, pos, n_buffer, delay, dst, src, n_samples);
buffer, pos, n_buffer, delay, dst, src, n_samples, fb, ff);
}
#endif /* SPA_FGA_DSP_H */

View file

@ -1228,7 +1228,7 @@ struct delay_impl {
struct spa_log *log;
unsigned long rate;
float *port[4];
float *port[6];
float delay;
uint32_t delay_samples;
@ -1334,7 +1334,8 @@ static void delay_run(void * Instance, unsigned long SampleCount)
}
if (in != NULL && out != NULL) {
spa_fga_dsp_delay(impl->dsp, impl->buffer, &impl->ptr, impl->buffer_samples,
impl->delay_samples, out, in, SampleCount);
impl->delay_samples, out, in, SampleCount,
impl->port[4][0], impl->port[5][0]);
}
if (impl->port[3] != NULL)
impl->port[3][0] = impl->latency;
@ -1359,6 +1360,16 @@ static struct spa_fga_port delay_ports[] = {
.hint = SPA_FGA_HINT_LATENCY,
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
},
{ .index = 4,
.name = "Feedback",
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
.def = 0.0f, .min = -10.0f, .max = 10.0f
},
{ .index = 5,
.name = "Feedforward",
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
.def = 0.0f, .min = -10.0f, .max = 10.0f
},
};
static const struct spa_fga_descriptor delay_desc = {

View file

@ -420,11 +420,12 @@ extern struct spa_handle_factory spa_filter_graph_factory;
*
* ### Delay
*
* The delay can be used to delay a signal in time.
* The delay can be used to delay a signal in time. With the Feedback and Feedforward
* controls it can also be used as a comb and an allpass filter.
*
* The delay has an input port "In" and an output port "Out". It also has
* a "Delay (s)" control port. It requires a config section in the node declaration
* in this format:
* a "Delay (s)" control port and a "Feedback" and "Feedforward" port. It requires a
* config section in the node declaration in this format:
*
*\code{.unparsed}
* filter.graph = {
@ -439,6 +440,8 @@ extern struct spa_handle_factory spa_filter_graph_factory;
* }
* control = {
* "Delay (s)" = ...
* "Feedback" = ...
* "Feedforward" = ...
* }
* ...
* }
@ -452,6 +455,10 @@ extern struct spa_handle_factory spa_filter_graph_factory;
* - `latency` the latency in seconds. This is 0 by default but in some cases
* the delay can be used to introduce latency with this option.
*
* With the "Feedback" port one can create a comb filter. With the "Feedback"
* port and "Feedforward" port set to A and -A respectively, one can create
* an allpass filter. These settings can be used to create custom reverb units.
*
* ### Invert
*
* The invert plugin can be used to invert the phase of the signal.