mixer-dsp: add support for fade-in and fade-out

When the port IO_Buffers is set, do a fade-in of the next buffer data
into the final mixed output.

When the port IO Buffers in cleared, do a fade-out of that last sample.

Add a zeroramp.duration property that controls the length in seconds of
the fade-in/out curve. By default, set this to 5ms.

The fade-in and fade-outs avoid popping noise from sudden DC voltage
changes when ports a linked and unlinked.

It will also trigger when the upstream peer is paused, because that will
also remove the IO Buffers from the mixer ports.
This commit is contained in:
Wim Taymans 2026-06-17 16:56:53 +02:00
parent 9a19091ac7
commit 214b9f6daa
2 changed files with 136 additions and 45 deletions

View file

@ -54,7 +54,7 @@ static int setup_context(struct context *ctx)
size_t size; size_t size;
int res; int res;
struct spa_support support[1]; struct spa_support support[1];
struct spa_dict_item items[9]; struct spa_dict_item items[11];
const struct spa_handle_factory *factory; const struct spa_handle_factory *factory;
void *iface; void *iface;
@ -79,10 +79,12 @@ static int setup_context(struct context *ctx)
items[6] = SPA_DICT_ITEM_INIT("channelmix.center-level", "0.707106781"); items[6] = SPA_DICT_ITEM_INIT("channelmix.center-level", "0.707106781");
items[7] = SPA_DICT_ITEM_INIT("channelmix.surround-level", "0.707106781"); items[7] = SPA_DICT_ITEM_INIT("channelmix.surround-level", "0.707106781");
items[8] = SPA_DICT_ITEM_INIT("channelmix.lfe-level", "0.5"); items[8] = SPA_DICT_ITEM_INIT("channelmix.lfe-level", "0.5");
items[9] = SPA_DICT_ITEM_INIT("zeroramp.gap", "0");
items[10] = SPA_DICT_ITEM_INIT("zeroramp.duration", "0.0");
res = spa_handle_factory_init(factory, res = spa_handle_factory_init(factory,
ctx->convert_handle, ctx->convert_handle,
&SPA_DICT_INIT(items, 9), &SPA_DICT_INIT(items, 11),
support, 1); support, 1);
spa_assert_se(res >= 0); spa_assert_se(res >= 0);

View file

@ -5,6 +5,7 @@
#include <errno.h> #include <errno.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include <math.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <spa/support/plugin.h> #include <spa/support/plugin.h>
@ -32,6 +33,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.mixer-dsp");
#define MAX_DATAS SPA_AUDIO_MAX_CHANNELS #define MAX_DATAS SPA_AUDIO_MAX_CHANNELS
#define MAX_PORTS 512 #define MAX_PORTS 512
#define MAX_ALIGN MIX_OPS_MAX_ALIGN #define MAX_ALIGN MIX_OPS_MAX_ALIGN
#define MAX_CURVE 4096u
#define PORT_DEFAULT_VOLUME 1.0 #define PORT_DEFAULT_VOLUME 1.0
#define PORT_DEFAULT_MUTE false #define PORT_DEFAULT_MUTE false
@ -85,6 +87,14 @@ struct port {
struct spa_list mix_link; struct spa_list mix_link;
bool active:1; bool active:1;
uint32_t ramp_pos;
float history[1];
};
struct ramp_info {
const float *data;
struct port *port;
int ramp_dir;
}; };
struct impl { struct impl {
@ -117,11 +127,16 @@ struct impl {
struct buffer *mix_buffers[MAX_PORTS]; struct buffer *mix_buffers[MAX_PORTS];
const void *mix_datas[MAX_PORTS]; const void *mix_datas[MAX_PORTS];
struct ramp_info ramp_info[MAX_PORTS];
int n_formats; int n_formats;
struct spa_audio_info format; struct spa_audio_info format;
uint32_t stride; uint32_t stride;
float curve[MAX_CURVE];
uint32_t n_curve;
float zeroramp_duration;
unsigned int have_format:1; unsigned int have_format:1;
unsigned int started:1; unsigned int started:1;
@ -246,13 +261,15 @@ impl_node_set_callbacks(void *object,
static struct port *get_free_port(struct impl *this) static struct port *get_free_port(struct impl *this)
{ {
struct port *port; struct port *port, *tmp;
if (!spa_list_is_empty(&this->free_list)) { spa_list_for_each_safe(port, tmp, &this->free_list, link) {
port = spa_list_first(&this->free_list, struct port, link); if (!port->active) {
spa_list_remove(&port->link); spa_list_remove(&port->link);
} else { spa_memzero(port, sizeof(struct port));
port = calloc(1, sizeof(struct port)); return port;
} }
}
port = calloc(1, sizeof(struct port));
return port; return port;
} }
@ -316,7 +333,6 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_
this->have_format = false; this->have_format = false;
} }
clear_buffers(this, port); clear_buffers(this, port);
spa_memzero(port, sizeof(struct port));
spa_list_append(&this->free_list, &port->link); spa_list_append(&this->free_list, &port->link);
spa_log_debug(this->log, "%p: remove port %d:%d", this, spa_log_debug(this->log, "%p: remove port %d:%d", this,
@ -729,14 +745,12 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq,
{ {
struct io_info *info = user_data; struct io_info *info = user_data;
struct port *port = info->port; struct port *port = info->port;
struct impl *impl = info->impl;
if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) { if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) {
port->io[0] = NULL; port->io[0] = NULL;
port->io[1] = NULL; port->io[1] = NULL;
if (port->active) { port->ramp_pos = impl->n_curve;
spa_list_remove(&port->mix_link);
port->active = false;
}
} else { } else {
if (info->size >= sizeof(struct spa_io_async_buffers)) { if (info->size >= sizeof(struct spa_io_async_buffers)) {
struct spa_io_async_buffers *ab = info->data; struct spa_io_async_buffers *ab = info->data;
@ -746,6 +760,7 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq,
port->io[0] = info->data; port->io[0] = info->data;
port->io[1] = info->data; port->io[1] = info->data;
} }
port->ramp_pos = 0;
if (port->direction == SPA_DIRECTION_INPUT && !port->active) { if (port->direction == SPA_DIRECTION_INPUT && !port->active) {
spa_list_append(&info->impl->mix_list, &port->mix_link); spa_list_append(&info->impl->mix_list, &port->mix_link);
port->active = true; port->active = true;
@ -803,14 +818,46 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
return queue_buffer(this, port, &port->buffers[buffer_id]); return queue_buffer(this, port, &port->buffers[buffer_id]);
} }
static void ramp_up(struct impl *this, float *dst, uint32_t size, struct ramp_info *ri)
{
uint32_t i, c;
struct port *port = ri->port;
spa_log_info(this->log, "fade-in %u/%u", port->ramp_pos, this->n_curve);
for (c = port->ramp_pos, i = 0; i < size && c < this->n_curve; i++, c++)
dst[i] += ri->data[i] * this->curve[c];
for (; i < size; i++)
dst[i] += ri->data[i];
port->ramp_pos = c;
}
static void ramp_down(struct impl *this, float *dst, uint32_t size, struct ramp_info *ri)
{
uint32_t i, c;
struct port *port = ri->port;
float last_sample = port->history[0];
spa_log_info(this->log, "fade-out %u %f", port->ramp_pos, last_sample);
for (c = port->ramp_pos, i = 0; i < size && c > 0; i++, c--)
dst[i] += last_sample * this->curve[c-1];
port->ramp_pos = c;
if (c == 0 && port->active) {
spa_list_remove(&port->mix_link);
port->active = false;
}
}
static int impl_node_process(void *object) static int impl_node_process(void *object)
{ {
struct impl *this = object; struct impl *this = object;
struct port *outport, *inport; struct port *outport, *inport;
struct spa_io_buffers *outio; struct spa_io_buffers *outio;
uint32_t n_buffers, maxsize; uint32_t n_buffers, maxsize, n_ramps, i;
struct buffer **buffers; struct buffer *last_buffer = NULL;
struct buffer *outb; struct buffer *outb;
struct ramp_info *ramps;
const void **datas; const void **datas;
uint32_t cycle = this->position->clock.cycle & 1; uint32_t cycle = this->position->clock.cycle & 1;
struct spa_data *d; struct spa_data *d;
@ -833,43 +880,65 @@ static int impl_node_process(void *object)
outio->buffer_id = SPA_ID_INVALID; outio->buffer_id = SPA_ID_INVALID;
} }
buffers = this->mix_buffers;
datas = this->mix_datas; datas = this->mix_datas;
n_buffers = 0; ramps = this->ramp_info;
n_buffers = n_ramps = 0;
maxsize = UINT32_MAX; maxsize = UINT32_MAX;
spa_list_for_each(inport, &this->mix_list, mix_link) { spa_list_for_each(inport, &this->mix_list, mix_link) {
struct spa_io_buffers *inio = inport->io[cycle]; struct spa_io_buffers *inio;
struct buffer *inb; struct buffer *inb = NULL;
struct spa_data *bd; struct spa_data *bd;
uint32_t size, offs; uint32_t size, offs;
float *s;
if (inio->buffer_id >= inport->n_buffers || if (SPA_UNLIKELY((inio = inport->io[cycle]) == NULL)) {
inio->status != SPA_STATUS_HAVE_DATA) { spa_log_trace_fp(this->log, "%p: skip input id:%d io:%p/%p/%d ramp:%d",
spa_log_trace_fp(this->log, "%p: skip input idx:%d " this, inport->id, inport->io[0], inport->io[1], cycle,
"io:%p status:%d buf_id:%d n_buffers:%d", this, inport->ramp_pos);
inport->id, inio, inio->status, inio->buffer_id, inport->n_buffers);
continue;
} }
else if (inio->buffer_id >= inport->n_buffers ||
inio->status != SPA_STATUS_HAVE_DATA) {
spa_log_trace_fp(this->log, "%p: skip input id:%d "
"io:%p status:%d buf_id:%d n_buffers:%d ramp:%d", this,
inport->id, inio, inio->status, inio->buffer_id, inport->n_buffers,
inport->ramp_pos);
} else {
inb = &inport->buffers[inio->buffer_id]; inb = &inport->buffers[inio->buffer_id];
}
if (inb != NULL) {
bd = &inb->buffer->datas[0]; bd = &inb->buffer->datas[0];
offs = SPA_MIN(bd->chunk->offset, bd->maxsize); offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); size = SPA_MIN(bd->maxsize - offs, bd->chunk->size);
maxsize = SPA_MIN(maxsize, size); maxsize = SPA_MIN(maxsize, size);
s = SPA_PTROFF(bd->data, offs, float);
spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d/%d %d:%d/%d %u", this, spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d/%d %d:%d/%d %u", this,
inport->id, inio, outio, inio->status, inio->buffer_id, inport->n_buffers, inport->id, inio, outio, inio->status, inio->buffer_id, inport->n_buffers,
offs, size, (int)sizeof(float), offs, size, (int)sizeof(float),
bd->chunk->flags); bd->chunk->flags);
if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) { if (SPA_UNLIKELY(inport->ramp_pos < this->n_curve)) {
datas[n_buffers] = SPA_PTROFF(bd->data, offs, void); /* new port */
buffers[n_buffers++] = inb; struct ramp_info *ri = &ramps[n_ramps++];
ri->port = inport;
ri->ramp_dir = 1;
ri->data = s;
} else {
datas[n_buffers++] = s;
last_buffer = inb;
} }
if (size >= sizeof(float))
inport->history[0] = s[size/sizeof(float)-1];
inio->status = SPA_STATUS_NEED_DATA; inio->status = SPA_STATUS_NEED_DATA;
} else if (inport->ramp_pos > 0) {
/* removed port, ramp down */
struct ramp_info *ri = &ramps[n_ramps++];
ri->port = inport;
ri->ramp_dir = -1;
}
} }
outb = dequeue_buffer(this, outport); outb = dequeue_buffer(this, outport);
@ -882,26 +951,37 @@ static int impl_node_process(void *object)
suppressed, outport->n_buffers); suppressed, outport->n_buffers);
return -EPIPE; return -EPIPE;
} }
d = outb->buf.datas; d = outb->buf.datas;
if (n_buffers == 1 && SPA_FLAG_IS_SET(d[0].flags, SPA_DATA_FLAG_DYNAMIC)) { if (n_buffers == 1 && SPA_FLAG_IS_SET(d[0].flags, SPA_DATA_FLAG_DYNAMIC) && n_ramps == 0) {
spa_log_trace_fp(this->log, "%p: %d passthrough", this, n_buffers); spa_log_trace_fp(this->log, "%p: %d passthrough", this, n_buffers);
*outb->buffer = *buffers[0]->buffer; *outb->buffer = *last_buffer->buffer;
} else { } else {
bool empty = n_buffers == 0 && n_ramps == 0;
float *dst = d[0].data;
uint32_t dst_samples;
*outb->buffer = outb->buf; *outb->buffer = outb->buf;
maxsize = SPA_MIN(maxsize, d[0].maxsize); maxsize = SPA_MIN(maxsize, d[0].maxsize);
dst_samples = maxsize / sizeof(float);
d[0].chunk->offset = 0; d[0].chunk->offset = 0;
d[0].chunk->size = maxsize; d[0].chunk->size = maxsize;
d[0].chunk->stride = sizeof(float); d[0].chunk->stride = sizeof(float);
SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0); SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, empty);
spa_log_trace_fp(this->log, "%p: %d mix %d", this, n_buffers, maxsize); spa_log_trace_fp(this->log, "%p: %d mix %d %u", this, n_buffers, maxsize, n_ramps);
mix_ops_process(&this->ops, d[0].data, mix_ops_process(&this->ops, dst, datas, n_buffers, dst_samples);
datas, n_buffers, maxsize / sizeof(float));
for (i = 0; i < n_ramps; i++) {
struct ramp_info *ri = &ramps[i];
if (ri->ramp_dir < 0)
ramp_down(this, dst, dst_samples, ri);
else
ramp_up(this, dst, dst_samples, ri);
}
} }
outio->buffer_id = outb->id; outio->buffer_id = outb->id;
@ -1005,13 +1085,22 @@ impl_init(const struct spa_handle_factory *factory,
this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu)); this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
} }
this->zeroramp_duration = 0.005f;
for (i = 0; info && i < info->n_items; i++) { for (i = 0; info && i < info->n_items; i++) {
const char *k = info->items[i].key; const char *k = info->items[i].key;
const char *s = info->items[i].value; const char *s = info->items[i].value;
if (spa_streq(k, "clock.quantum-limit")) if (spa_streq(k, "clock.quantum-limit"))
spa_atou32(s, &this->quantum_limit, 0); spa_atou32(s, &this->quantum_limit, 0);
if (spa_streq(k, "zeroramp.duration"))
spa_atof(s, &this->zeroramp_duration);
} }
this->n_curve = SPA_MIN((uint32_t)(this->zeroramp_duration * 48000), MAX_CURVE);
/* S-shaped curve */
for (i = 0; i < this->n_curve; i++)
this->curve[i] = (float)(0.5 + 0.5 * cos(M_PI + M_PI * i / this->n_curve));
spa_hook_list_init(&this->hooks); spa_hook_list_init(&this->hooks);
spa_list_init(&this->port_list); spa_list_init(&this->port_list);
spa_list_init(&this->free_list); spa_list_init(&this->free_list);