From 214b9f6daa98223ff9b65f7c0756af9926c47032 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 17 Jun 2026 16:56:53 +0200 Subject: [PATCH] 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. --- spa/plugins/audioconvert/test-audioconvert.c | 6 +- spa/plugins/audiomixer/mixer-dsp.c | 175 ++++++++++++++----- 2 files changed, 136 insertions(+), 45 deletions(-) diff --git a/spa/plugins/audioconvert/test-audioconvert.c b/spa/plugins/audioconvert/test-audioconvert.c index de18d524b..905ce412c 100644 --- a/spa/plugins/audioconvert/test-audioconvert.c +++ b/spa/plugins/audioconvert/test-audioconvert.c @@ -54,7 +54,7 @@ static int setup_context(struct context *ctx) size_t size; int res; struct spa_support support[1]; - struct spa_dict_item items[9]; + struct spa_dict_item items[11]; const struct spa_handle_factory *factory; 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[7] = SPA_DICT_ITEM_INIT("channelmix.surround-level", "0.707106781"); 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, ctx->convert_handle, - &SPA_DICT_INIT(items, 9), + &SPA_DICT_INIT(items, 11), support, 1); spa_assert_se(res >= 0); diff --git a/spa/plugins/audiomixer/mixer-dsp.c b/spa/plugins/audiomixer/mixer-dsp.c index f23851507..9ad4a69a0 100644 --- a/spa/plugins/audiomixer/mixer-dsp.c +++ b/spa/plugins/audiomixer/mixer-dsp.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.mixer-dsp"); #define MAX_DATAS SPA_AUDIO_MAX_CHANNELS #define MAX_PORTS 512 #define MAX_ALIGN MIX_OPS_MAX_ALIGN +#define MAX_CURVE 4096u #define PORT_DEFAULT_VOLUME 1.0 #define PORT_DEFAULT_MUTE false @@ -85,6 +87,14 @@ struct port { struct spa_list mix_link; bool active:1; + uint32_t ramp_pos; + float history[1]; +}; + +struct ramp_info { + const float *data; + struct port *port; + int ramp_dir; }; struct impl { @@ -117,11 +127,16 @@ struct impl { struct buffer *mix_buffers[MAX_PORTS]; const void *mix_datas[MAX_PORTS]; + struct ramp_info ramp_info[MAX_PORTS]; int n_formats; struct spa_audio_info format; uint32_t stride; + float curve[MAX_CURVE]; + uint32_t n_curve; + float zeroramp_duration; + unsigned int have_format:1; unsigned int started:1; @@ -246,13 +261,15 @@ impl_node_set_callbacks(void *object, static struct port *get_free_port(struct impl *this) { - struct port *port; - if (!spa_list_is_empty(&this->free_list)) { - port = spa_list_first(&this->free_list, struct port, link); - spa_list_remove(&port->link); - } else { - port = calloc(1, sizeof(struct port)); + struct port *port, *tmp; + spa_list_for_each_safe(port, tmp, &this->free_list, link) { + if (!port->active) { + spa_list_remove(&port->link); + spa_memzero(port, sizeof(struct port)); + return port; + } } + port = calloc(1, sizeof(struct port)); return port; } @@ -316,7 +333,6 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ this->have_format = false; } clear_buffers(this, port); - spa_memzero(port, sizeof(struct port)); spa_list_append(&this->free_list, &port->link); 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 port *port = info->port; + struct impl *impl = info->impl; if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) { port->io[0] = NULL; port->io[1] = NULL; - if (port->active) { - spa_list_remove(&port->mix_link); - port->active = false; - } + port->ramp_pos = impl->n_curve; } else { if (info->size >= sizeof(struct spa_io_async_buffers)) { 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[1] = info->data; } + port->ramp_pos = 0; if (port->direction == SPA_DIRECTION_INPUT && !port->active) { spa_list_append(&info->impl->mix_list, &port->mix_link); 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]); } +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) { struct impl *this = object; struct port *outport, *inport; struct spa_io_buffers *outio; - uint32_t n_buffers, maxsize; - struct buffer **buffers; + uint32_t n_buffers, maxsize, n_ramps, i; + struct buffer *last_buffer = NULL; struct buffer *outb; + struct ramp_info *ramps; const void **datas; uint32_t cycle = this->position->clock.cycle & 1; struct spa_data *d; @@ -833,43 +880,65 @@ static int impl_node_process(void *object) outio->buffer_id = SPA_ID_INVALID; } - buffers = this->mix_buffers; datas = this->mix_datas; - n_buffers = 0; + ramps = this->ramp_info; + n_buffers = n_ramps = 0; maxsize = UINT32_MAX; spa_list_for_each(inport, &this->mix_list, mix_link) { - struct spa_io_buffers *inio = inport->io[cycle]; - struct buffer *inb; + struct spa_io_buffers *inio; + struct buffer *inb = NULL; struct spa_data *bd; uint32_t size, offs; + float *s; - if (inio->buffer_id >= inport->n_buffers || + if (SPA_UNLIKELY((inio = inport->io[cycle]) == NULL)) { + spa_log_trace_fp(this->log, "%p: skip input id:%d io:%p/%p/%d ramp:%d", + this, inport->id, inport->io[0], inport->io[1], cycle, + inport->ramp_pos); + } + else if (inio->buffer_id >= inport->n_buffers || inio->status != SPA_STATUS_HAVE_DATA) { - spa_log_trace_fp(this->log, "%p: skip input idx:%d " - "io:%p status:%d buf_id:%d n_buffers:%d", this, - inport->id, inio, inio->status, inio->buffer_id, inport->n_buffers); - continue; + 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]; } + if (inb != NULL) { + bd = &inb->buffer->datas[0]; - inb = &inport->buffers[inio->buffer_id]; - bd = &inb->buffer->datas[0]; + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); + maxsize = SPA_MIN(maxsize, size); + s = SPA_PTROFF(bd->data, offs, float); - offs = SPA_MIN(bd->chunk->offset, bd->maxsize); - size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); - maxsize = SPA_MIN(maxsize, size); + 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, + offs, size, (int)sizeof(float), + bd->chunk->flags); - 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, - offs, size, (int)sizeof(float), - bd->chunk->flags); - - if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) { - datas[n_buffers] = SPA_PTROFF(bd->data, offs, void); - buffers[n_buffers++] = inb; + if (SPA_UNLIKELY(inport->ramp_pos < this->n_curve)) { + /* new port */ + 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; + } else if (inport->ramp_pos > 0) { + /* removed port, ramp down */ + struct ramp_info *ri = &ramps[n_ramps++]; + ri->port = inport; + ri->ramp_dir = -1; } - inio->status = SPA_STATUS_NEED_DATA; } outb = dequeue_buffer(this, outport); @@ -882,26 +951,37 @@ static int impl_node_process(void *object) suppressed, outport->n_buffers); return -EPIPE; } - 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); - *outb->buffer = *buffers[0]->buffer; + *outb->buffer = *last_buffer->buffer; } else { + bool empty = n_buffers == 0 && n_ramps == 0; + float *dst = d[0].data; + uint32_t dst_samples; + *outb->buffer = outb->buf; maxsize = SPA_MIN(maxsize, d[0].maxsize); + dst_samples = maxsize / sizeof(float); d[0].chunk->offset = 0; d[0].chunk->size = maxsize; 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, - datas, n_buffers, maxsize / sizeof(float)); + mix_ops_process(&this->ops, dst, datas, n_buffers, dst_samples); + + 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; @@ -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->zeroramp_duration = 0.005f; + for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; const char *s = info->items[i].value; if (spa_streq(k, "clock.quantum-limit")) 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_list_init(&this->port_list); spa_list_init(&this->free_list);