Index | Thread | Search

From:
Alexandre Ratchov <alex@caoua.org>
Subject:
libsndio: Add an interface to report underruns
To:
tech@openbsd.org
Date:
Mon, 29 Dec 2025 12:13:46 +0100

Download raw body.

Thread
This diff adds the new sio_onxrun(3) function that can be used to
register a callback function that will be called on buffer underrun.

There are cases where a program may use an external audio clock
(ex. an RTP stream) and resample to make the local audio rate
match the remote rate to keep the latency constant. To do so,
the program must measure continuously the clock drift and calculate
the resampling ratio. Upon underrun, such programs must restart
the measurements, hence the need for this new interface.

The audio/sndiortp port will be the first user of this function,
but other programs could benefit from this as well.

This function will be necessary to make sndiod combine multiple audio
devices into a single one (ex. combine webcam's mic with the internal
speaker).

OK?

Index: include//sndio.h
===================================================================
RCS file: /cvs/src/include/sndio.h,v
diff -u -p -u -p -r1.15 sndio.h
--- include//sndio.h	24 May 2024 15:10:26 -0000	1.15
+++ include//sndio.h	27 Dec 2025 17:42:51 -0000
@@ -169,6 +169,7 @@ int sio_setpar(struct sio_hdl *, struct 
 int sio_getpar(struct sio_hdl *, struct sio_par *);
 int sio_getcap(struct sio_hdl *, struct sio_cap *);
 void sio_onmove(struct sio_hdl *, void (*)(void *, int), void *);
+void sio_onxrun(struct sio_hdl *, void (*)(void *), void *);
 size_t sio_write(struct sio_hdl *, const void *, size_t);
 size_t sio_read(struct sio_hdl *, void *, size_t);
 int sio_start(struct sio_hdl *);
Index: lib/libsndio/Symbols.map
===================================================================
RCS file: /cvs/src/lib/libsndio/Symbols.map,v
diff -u -p -u -p -r1.3 Symbols.map
--- lib/libsndio/Symbols.map	29 Apr 2022 08:30:48 -0000	1.3
+++ lib/libsndio/Symbols.map	27 Dec 2025 17:42:51 -0000
@@ -18,6 +18,7 @@
 		sio_eof;
 		sio_setvol;
 		sio_onvol;
+		sio_onxrun;
 
 		mio_open;
 		mio_close;
Index: lib/libsndio/amsg.h
===================================================================
RCS file: /cvs/src/lib/libsndio/amsg.h,v
diff -u -p -u -p -r1.16 amsg.h
--- lib/libsndio/amsg.h	24 May 2024 15:16:09 -0000	1.16
+++ lib/libsndio/amsg.h	27 Dec 2025 17:42:51 -0000
@@ -80,6 +80,7 @@ struct amsg {
 #define AMSG_CTLSET	14	/* set control value */
 #define AMSG_CTLSYNC	15	/* end of controls descriptions */
 #define AMSG_CTLSUB	16	/* ondesc/onctl subscription */
+#define AMSG_XRUN	17	/* notification about xruns */
 	uint32_t cmd;
 	uint32_t __pad;
 	union {
@@ -104,6 +105,9 @@ struct amsg {
 #define AMSG_DATAMAX	0x1000
 			uint32_t size;
 		} data;
+		struct amsg_start {
+			uint8_t xrunnotify;
+		} start;
 		struct amsg_stop {
 			uint8_t drain;
 		} stop;
@@ -113,6 +117,9 @@ struct amsg {
 		struct amsg_vol {
 			uint32_t ctl;
 		} vol;
+		struct amsg_xrun {
+			uint8_t state;
+		} xrun;
 		struct amsg_hello {
 			uint16_t mode;		/* bitmap of MODE_XXX */
 #define AMSG_VERSION	7
Index: lib/libsndio/shlib_version
===================================================================
RCS file: /cvs/src/lib/libsndio/shlib_version,v
diff -u -p -u -p -r1.15 shlib_version
--- lib/libsndio/shlib_version	16 Jul 2025 15:33:05 -0000	1.15
+++ lib/libsndio/shlib_version	27 Dec 2025 17:42:51 -0000
@@ -1,2 +1,2 @@
 major=8
-minor=0
+minor=1
Index: lib/libsndio/sio.c
===================================================================
RCS file: /cvs/src/lib/libsndio/sio.c,v
diff -u -p -u -p -r1.27 sio.c
--- lib/libsndio/sio.c	29 Apr 2022 08:30:48 -0000	1.27
+++ lib/libsndio/sio.c	27 Dec 2025 17:42:51 -0000
@@ -85,6 +85,7 @@ _sio_create(struct sio_hdl *hdl, struct 
 	hdl->started = 0;
 	hdl->eof = 0;
 	hdl->move_cb = NULL;
+	hdl->xrun_cb = NULL;
 	hdl->vol_cb = NULL;
 }
 
@@ -123,6 +124,7 @@ sio_start(struct sio_hdl *hdl)
 	if (!hdl->ops->start(hdl))
 		return 0;
 	hdl->started = 1;
+	hdl->xrun = 0;
 	return 1;
 }
 
@@ -517,6 +519,7 @@ _sio_onmove_cb(struct sio_hdl *hdl, int 
 #endif
 	if (hdl->move_cb)
 		hdl->move_cb(hdl->move_addr, delta);
+	hdl->xrun = 0;
 }
 
 int
@@ -553,4 +556,27 @@ _sio_onvol_cb(struct sio_hdl *hdl, unsig
 {
 	if (hdl->vol_cb)
 		hdl->vol_cb(hdl->vol_addr, ctl);
+}
+
+void
+sio_onxrun(struct sio_hdl *hdl, void (*cb)(void *), void *addr)
+{
+	if (hdl->started) {
+		DPRINTF("sio_onxrun: already started\n");
+		hdl->eof = 1;
+		return;
+	}
+	hdl->xrun_cb = cb;
+	hdl->xrun_addr = addr;
+}
+
+void
+_sio_onxrun_cb(struct sio_hdl *hdl)
+{
+	if (!hdl->xrun) {
+		hdl->xrun = 1;
+		if (hdl->xrun_cb)
+			hdl->xrun_cb(hdl->xrun_addr);
+	}
+	DPRINTFN(1, "sndio: xrun\n");
 }
Index: lib/libsndio/sio_aucat.c
===================================================================
RCS file: /cvs/src/lib/libsndio/sio_aucat.c,v
diff -u -p -u -p -r1.21 sio_aucat.c
--- lib/libsndio/sio_aucat.c	29 Apr 2022 08:30:48 -0000	1.21
+++ lib/libsndio/sio_aucat.c	27 Dec 2025 17:42:51 -0000
@@ -115,6 +115,10 @@ sio_aucat_runmsg(struct sio_aucat_hdl *h
 			hdl->delta = 0;
 		}
 		break;
+	case AMSG_XRUN:
+		DPRINTFN(3, "aucat: xrun\n");
+		_sio_onxrun_cb(&hdl->sio);
+		break;
 	case AMSG_SETVOL:
 		ctl = ntohl(hdl->aucat.rmsg.u.vol.ctl);
 		hdl->curvol = hdl->reqvol = ctl;
@@ -196,6 +200,7 @@ sio_aucat_start(struct sio_hdl *sh)
 
 	AMSG_INIT(&hdl->aucat.wmsg);
 	hdl->aucat.wmsg.cmd = htonl(AMSG_START);
+	hdl->aucat.wmsg.u.start.xrunnotify = 1;
 	hdl->aucat.wtodo = sizeof(struct amsg);
 	if (!_aucat_wmsg(&hdl->aucat, &hdl->sio.eof))
 		return 0;
Index: lib/libsndio/sio_open.3
===================================================================
RCS file: /cvs/src/lib/libsndio/sio_open.3,v
diff -u -p -u -p -r1.58 sio_open.3
--- lib/libsndio/sio_open.3	13 Jun 2025 18:34:00 -0000	1.58
+++ lib/libsndio/sio_open.3	27 Dec 2025 17:42:51 -0000
@@ -588,6 +588,21 @@ might overrun; in this case recorded dat
 Similarly if the application cannot provide data to play
 fast enough, the play buffer underruns and silence is played
 instead.
+.Pp
+The
+.Fn sio_onxrun
+function can be used to register the
+.Fn cb
+callback function called at buffer underrun or overrun.
+It is called by
+.Fn sio_read ,
+.Fn sio_write ,
+and
+.Fn sio_revents .
+The value of the
+.Fa arg
+pointer is passed to the callback and can contain anything.
+.Pp
 Depending on the
 .Fa xrun
 parameter of the
Index: lib/libsndio/sio_priv.h
===================================================================
RCS file: /cvs/src/lib/libsndio/sio_priv.h,v
diff -u -p -u -p -r1.12 sio_priv.h
--- lib/libsndio/sio_priv.h	21 May 2024 06:07:06 -0000	1.12
+++ lib/libsndio/sio_priv.h	27 Dec 2025 17:42:51 -0000
@@ -30,6 +30,8 @@ struct sio_hdl {
 	void *move_addr;		/* user priv. data for move_cb */
 	void (*vol_cb)(void *, unsigned); /* call-back for volume changes */
 	void *vol_addr;			/* user priv. data for vol_cb */
+	void (*xrun_cb)(void *);	/* call-back for xruns */
+	void *xrun_addr;		/* user priv. data for xrun_cb */
 	unsigned mode;			/* SIO_PLAY | SIO_REC */
 	int started;			/* true if started */
 	int nbio;			/* true if non-blocking io */
@@ -38,6 +40,7 @@ struct sio_hdl {
 	int wsil;			/* silence to play */
 	int rused;			/* bytes used in read buffer */
 	int wused;			/* bytes used in write buffer */
+	int xrun;			/* xrun reported */
 	long long cpos;			/* clock since start */
 	struct sio_par par;
 #ifdef DEBUG
@@ -71,6 +74,7 @@ struct sio_hdl *_sio_sun_open(const char
 void _sio_create(struct sio_hdl *, struct sio_ops *, unsigned, int);
 void _sio_onmove_cb(struct sio_hdl *, int);
 void _sio_onvol_cb(struct sio_hdl *, unsigned);
+void _sio_onxrun_cb(struct sio_hdl *);
 #ifdef DEBUG
 void _sio_printpos(struct sio_hdl *);
 #endif
Index: lib/libsndio/sio_sun.c
===================================================================
RCS file: /cvs/src/lib/libsndio/sio_sun.c,v
diff -u -p -u -p -r1.32 sio_sun.c
--- lib/libsndio/sio_sun.c	11 Nov 2025 11:08:10 -0000	1.32
+++ lib/libsndio/sio_sun.c	27 Dec 2025 17:42:51 -0000
@@ -542,6 +542,9 @@ sio_sun_xrun(struct sio_sun_hdl *hdl)
 	 */
 	if (!sio_sun_flush(&hdl->sio))
 		return 0;
+
+	_sio_onxrun_cb(&hdl->sio);
+
 	if (!sio_sun_start(&hdl->sio))
 		return 0;
 
Index: usr.bin/sndiod/dev.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/dev.c,v
diff -u -p -u -p -r1.126 dev.c
--- usr.bin/sndiod/dev.c	26 Nov 2025 08:40:16 -0000	1.126
+++ usr.bin/sndiod/dev.c	27 Dec 2025 17:42:51 -0000
@@ -28,6 +28,7 @@
 #include "utils.h"
 
 void zomb_onmove(void *);
+void zomb_onxrun(void *);
 void zomb_onvol(void *);
 void zomb_fill(void *);
 void zomb_flush(void *);
@@ -64,6 +65,7 @@ int slot_skip(struct slot *);
 
 struct slotops zomb_slotops = {
 	zomb_onmove,
+	zomb_onxrun,
 	zomb_onvol,
 	zomb_fill,
 	zomb_flush,
@@ -91,6 +93,11 @@ zomb_onmove(void *arg)
 }
 
 void
+zomb_onxrun(void *arg)
+{
+}
+
+void
 zomb_onvol(void *arg)
 {
 }
@@ -635,9 +642,13 @@ dev_cycle(struct dev *d)
 			s->sub.buf.len - s->sub.buf.used <
 			s->round * s->sub.bpf)) {
 
+			if (!s->paused) {
 #ifdef DEBUG
-			logx(3, "slot%zu: xrun, pause cycle", s - slot_array);
+				logx(3, "slot%zu: xrun, paused", s - slot_array);
 #endif
+				s->paused = 1;
+				s->ops->onxrun(s->arg);
+			}
 			if (s->xrun == XRUN_IGNORE) {
 				s->delta -= s->round;
 				ps = &s->next;
@@ -654,7 +665,15 @@ dev_cycle(struct dev *d)
 #endif
 			}
 			continue;
+		} else {
+			if (s->paused) {
+#ifdef DEBUG
+				logx(3, "slot%zu: resumed\n", s - slot_array);
+#endif
+				s->paused = 0;
+			}
 		}
+
 		if ((s->mode & MODE_RECMASK) && !(s->pstate == SLOT_STOP)) {
 			if (s->sub.prime == 0) {
 				dev_sub_bcopy(d, s);
@@ -1588,6 +1607,7 @@ slot_start(struct slot *s)
 		s->sub.prime = d->bufsz / d->round;
 	}
 	s->skip = 0;
+	s->paused = 0;
 
 	/*
 	 * get the current position, the origin is when the first sample
Index: usr.bin/sndiod/dev.h
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/dev.h,v
diff -u -p -u -p -r1.50 dev.h
--- usr.bin/sndiod/dev.h	26 Nov 2025 08:40:16 -0000	1.50
+++ usr.bin/sndiod/dev.h	27 Dec 2025 17:42:51 -0000
@@ -40,6 +40,7 @@
 struct slotops
 {
 	void (*onmove)(void *);			/* clock tick */
+	void (*onxrun)(void *);			/* xrun */
 	void (*onvol)(void *);			/* tell client vol changed */
 	void (*fill)(void *);			/* request to fill a play block */
 	void (*flush)(void *);			/* request to flush a rec block */
@@ -100,6 +101,7 @@ struct slot {
 #define SLOT_RUN	3			/* buffer attached to device */
 #define SLOT_STOP	4			/* draining */
 	int pstate;
+	int paused;				/* paused because of xrun */
 
 	struct app *app;
 };
Index: usr.bin/sndiod/siofile.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/siofile.c,v
diff -u -p -u -p -r1.29 siofile.c
--- usr.bin/sndiod/siofile.c	11 Nov 2025 11:48:03 -0000	1.29
+++ usr.bin/sndiod/siofile.c	27 Dec 2025 17:42:51 -0000
@@ -36,6 +36,7 @@
 #define WATCHDOG_USEC	4000000		/* 4 seconds */
 
 void dev_sio_onmove(void *, int);
+void dev_sio_onxrun(void *);
 void dev_sio_timeout(void *);
 int dev_sio_pollfd(void *, struct pollfd *);
 int dev_sio_revents(void *, struct pollfd *);
@@ -74,6 +75,19 @@ dev_sio_onmove(void *arg, int delta)
 }
 
 void
+dev_sio_onxrun(void *arg)
+{
+	struct dev *d = arg;
+	struct slot *s;
+
+#ifdef DEBUG
+	logx(1, "%s: xrun", d->path);
+#endif
+	for (s = d->slot_list; s != NULL; s = s->next)
+		s->ops->onxrun(s->arg);
+}
+
+void
 dev_sio_timeout(void *arg)
 {
 	struct dev *d = arg;
@@ -213,6 +227,7 @@ dev_sio_open(struct dev *d)
 	if (d->mode & MODE_PLAY)
 		d->mode |= MODE_MON;
 	sio_onmove(d->sio.hdl, dev_sio_onmove, d);
+	sio_onxrun(d->sio.hdl, dev_sio_onxrun, d);
 	d->sio.file = file_new(&dev_sio_ops, d, "dev", sio_nfds(d->sio.hdl));
 	if (d->sioctl.hdl) {
 		d->sioctl.file = file_new(&dev_sioctl_ops, d, "mix",
Index: usr.bin/sndiod/sock.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/sock.c,v
diff -u -p -u -p -r1.55 sock.c
--- usr.bin/sndiod/sock.c	19 Jun 2025 20:16:34 -0000	1.55
+++ usr.bin/sndiod/sock.c	27 Dec 2025 17:42:51 -0000
@@ -40,6 +40,7 @@ void sock_slot_fill(void *);
 void sock_slot_flush(void *);
 void sock_slot_eof(void *);
 void sock_slot_onmove(void *);
+void sock_slot_onxrun(void *);
 void sock_slot_onvol(void *);
 void sock_midi_imsg(void *, unsigned char *, int);
 void sock_midi_omsg(void *, unsigned char *, int);
@@ -77,6 +78,7 @@ struct fileops sock_fileops = {
 
 struct slotops sock_slotops = {
 	sock_slot_onmove,
+	sock_slot_onxrun,
 	sock_slot_onvol,
 	sock_slot_fill,
 	sock_slot_flush,
@@ -244,6 +246,21 @@ sock_slot_onmove(void *arg)
 }
 
 void
+sock_slot_onxrun(void *arg)
+{
+	struct sock *f = (struct sock *)arg;
+	struct slot *s = f->slot;
+
+#ifdef DEBUG
+	logx(4, "slot%zu: onxrun: notify = %d", s - slot_array, f->xrunnotify);
+#endif
+	if (s->pstate != SOCK_START)
+		return;
+	if (f->xrunnotify)
+		f->xrunpending = 1;
+}
+
+void
 sock_slot_onvol(void *arg)
 {
 	struct sock *f = (struct sock *)arg;
@@ -301,6 +318,7 @@ sock_new(int fd)
 	f->midi = NULL;
 	f->ctlslot = NULL;
 	f->tickpending = 0;
+	f->xrunpending = 0;
 	f->fillpending = 0;
 	f->stoppending = 0;
 	f->wstate = SOCK_WIDLE;
@@ -309,6 +327,7 @@ sock_new(int fd)
 	f->rtodo = sizeof(struct amsg);
 	f->wmax = f->rmax = 0;
 	f->lastvol = -1;
+	f->xrunnotify = 0;
 	f->ctlops = 0;
 	f->ctlsyncpending = 0;
 	f->file = file_new(&sock_fileops, f, "sock", 1);
@@ -882,6 +901,9 @@ sock_execmsg(struct sock *f)
 			return 0;
 		}
 		f->tickpending = 0;
+		f->xrunpending = 0;
+		if (AMSG_ISSET(m->u.start.xrunnotify))
+			f->xrunnotify = m->u.start.xrunnotify;
 		f->stoppending = 0;
 		slot_start(s);
 		if (s->mode & MODE_PLAY) {
@@ -1161,6 +1183,18 @@ sock_buildmsg(struct sock *f)
 		 * slot->delta
 		 */
 		f->slot->delta = 0;
+		return 1;
+	}
+
+	if (f->xrunpending) {
+#ifdef DEBUG
+		logx(4, "sock %d: building XRUN message", f->fd);
+#endif
+		AMSG_INIT(&f->wmsg);
+		f->wmsg.cmd = htonl(AMSG_XRUN);
+		f->wtodo = sizeof(struct amsg);
+		f->wstate = SOCK_WMSG;
+		f->xrunpending = 0;
 		return 1;
 	}
 
Index: usr.bin/sndiod/sock.h
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/sock.h,v
diff -u -p -u -p -r1.9 sock.h
--- usr.bin/sndiod/sock.h	24 May 2024 15:16:09 -0000	1.9
+++ usr.bin/sndiod/sock.h	27 Dec 2025 17:42:51 -0000
@@ -50,6 +50,8 @@ struct sock {
 #define SOCK_STOP	4		/* draining rec buffers */
 	unsigned int pstate;		/* one of the above */
 	int tickpending;		/* tick waiting to be transmitted */
+	int xrunpending;		/* xrun waiting to be transmitted */
+	int xrunnotify;			/* client subscribed to xrun messages */
 	int fillpending;		/* flowctl waiting to be transmitted */
 	int stoppending;		/* last STOP ack to be sent */
 	unsigned int walign;		/* align written data to this */