Re: switching between audio devices with no playback interruption
On Tue, Sep 17, 2019 at 09:57:28AM +0200, Alexandre Ratchov wrote: > > > Regarding the race I was talking about, doesn't dev_reopen() close the > > current device before trying to open a new one, possibly finding an > > incompatible device or failing to open it? What happens in this case? > > After a SIGHUP, if the new device is incompatible, the old one is > reopened and sound continues. Now I understand your remark: oops! > close() and open() are called in the wrong order, so another process > could steal the device. I'll fix that. > Here's a new diff with the fix. Now, the new device is opened before closing the old one. I've fixed two other bugs in the pure audio logic. Index: dev.c === RCS file: /cvs/src/usr.bin/sndiod/dev.c,v retrieving revision 1.61 diff -u -p -u -p -r1.61 dev.c --- dev.c 19 Sep 2019 05:10:19 - 1.61 +++ dev.c 20 Sep 2019 07:17:54 - @@ -969,7 +969,8 @@ dev_new(char *path, struct aparams *par, return NULL; } d = xmalloc(sizeof(struct dev)); - d->path = xstrdup(path); + d->path_list = NULL; + namelist_add(&d->path_list, path); d->num = dev_sndnum++; d->opt_list = NULL; @@ -1174,6 +1175,74 @@ dev_close(struct dev *d) dev_freebufs(d); } +/* + * Close the device, but attempt to migrate everything to a new sndio + * device. + */ +int +dev_reopen(struct dev *d) +{ + struct slot *s; + long long pos; + unsigned int pstate; + int delta; + + /* not opened */ + if (d->pstate == DEV_CFG) + return 1; + + /* save state */ + delta = d->delta; + pstate = d->pstate; + + if (!dev_sio_reopen(d)) + return 0; + + /* reopen returns a stopped device */ + d->pstate = DEV_INIT; + + /* reallocate new buffers, with new parameters */ + dev_freebufs(d); + dev_allocbufs(d); + + /* +* adjust time positions, make anything go back delta ticks, so +* that the new device can start at zero +*/ + for (s = d->slot_list; s != NULL; s = s->next) { + pos = (long long)s->delta * d->round + s->delta_rem; + pos -= (long long)delta * s->round; + s->delta_rem = pos % (int)d->round; + s->delta = pos / (int)d->round; + if (log_level >= 3) { + slot_log(s); + log_puts(": adjusted: delta -> "); + log_puti(s->delta); + log_puts(", delta_rem -> "); + log_puti(s->delta_rem); + log_puts("\n"); + } + + /* reinitilize the format conversion chain */ + slot_initconv(s); + } + if (d->tstate == MMC_RUN) { + d->mtc.delta -= delta * MTC_SEC; + if (log_level >= 2) { + dev_log(d); + log_puts(": adjusted mtc: delta ->"); + log_puti(d->mtc.delta); + log_puts("\n"); + } + } + + /* start the device if needed */ + if (pstate == DEV_RUN) + dev_wakeup(d); + + return 1; +} + int dev_ref(struct dev *d) { @@ -1280,7 +1349,7 @@ dev_del(struct dev *d) } midi_del(d->midi); *p = d->next; - xfree(d->path); + namelist_clear(&d->path_list); xfree(d); } Index: dev.h === RCS file: /cvs/src/usr.bin/sndiod/dev.h,v retrieving revision 1.21 diff -u -p -u -p -r1.21 dev.h --- dev.h 12 Jul 2019 06:30:55 - 1.21 +++ dev.h 20 Sep 2019 07:17:54 - @@ -159,7 +159,7 @@ struct dev { #define DEV_INIT 1 /* stopped */ #define DEV_RUN2 /* playin & recording */ unsigned int pstate;/* one of above */ - char *path; /* sio path */ + struct name *path_list; /* * actual parameters and runtime state (i.e. once opened) @@ -201,6 +201,7 @@ extern struct dev *dev_list; void dev_log(struct dev *); void dev_close(struct dev *); +int dev_reopen(struct dev *); struct dev *dev_new(char *, struct aparams *, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int); struct dev *dev_bynum(int); Index: fdpass.c === RCS file: /cvs/src/usr.bin/sndiod/fdpass.c,v retrieving revision 1.6 diff -u -p -u -p -r1.6 fdpass.c --- fdpass.c28 Jun 2019 13:35:03 - 1.6 +++ fdpass.c20 Sep 2019 07:17:54 - @@ -300,6 +300,7 @@ fdpass_in_helper(void *arg) struct fdpass *f = arg; struct dev *d; struct port *p; + struct name *path; if (!fdpass
Re: switching between audio devices with no playback interruption
On Mon, Sep 16, 2019 at 03:44:32PM -0300, Martin Pieuchot wrote: > On 16/09/19(Mon) 19:31, Alexandre Ratchov wrote: > > On Sun, Sep 15, 2019 at 03:01:36PM -0300, Martin Pieuchot wrote: > > > On 29/08/19(Thu) 10:52, Alexandre Ratchov wrote: > > > > The diff below allows to specifiy "an alternate device" to use in case > > > > the audio device is disconnected. If so, programs continue playing > > > > using the other device. > > > > > > > > For instance, if dmesg contains: > > > > > > > > audio0 at azalia0 > > > > audio1 at uaudio0 > > > > > > > > and /etc/rc.conf.local contains: > > > > > > > > sndiod_flags=-f rsnd/0 -F rsnd/1 > > > > > > > > then sndiod will try to use the usb device by default. If it's > > > > disconnected, programs continue using the internal one. Note that > > > > there's no way to detect that the usb device is reconnected; > > > > > > Instead of doing a open/reopen dance in userland, which is inherently > > > racy, did you consider introducing a driver abstracting the various > > > audio(4) drivers attached? I'm thinking of something like wsmux(4) for > > > audio. This should allow us to transparently attach/detach usb audio > > > devices. > > > > I don't see any race conditions: once a device disappears, it is > > closed and another one is opened in place. This only relies on calls > > to open() and close() which are not racy. Unlike with hotplug(4), > > there's no concurency between multiple code-paths; it's all > > sequencial. > > I understand the logic for closing an existing device, but what about > plugging a new one? I understand that this is not the scope of the > diff, I wondering if that would fit in the model you're suggesting. > > It is just a matter of sending SIGHUP, so I could fake that when > uaudio(4) attaches for example? Yes. That's the best we can do with "unix file open/close semantics" only. Going further, if desired, would require some kind of kernel support. There's a full range of "clever" and complicated ways to handle hot-plugging automatically (and to configure it!). I don't know what's the best one, some experimenting is needed first. > Regarding the race I was talking about, doesn't dev_reopen() close the > current device before trying to open a new one, possibly finding an > incompatible device or failing to open it? What happens in this case? After a SIGHUP, if the new device is incompatible, the old one is reopened and sound continues. Now I understand your remark: oops! close() and open() are called in the wrong order, so another process could steal the device. I'll fix that. > > I've considered the wsmux-style device for audio. Multiple audio > > devices can't be combined, so the audio mux device would be a switch > > between the devices, which is roughly what the diff does. > > > > Switching between devices, when possible, is not trivial, because the > > state of one device must be restored on another device. It may require > > channel mapping and/or format conversions, in case the devices don't > > have the same capabilities. We don't want such complicated code to run > > with full kernel privileges and to have access to all the physical > > memory. FWIW, this was one of the reasons for moving the audio > > sub-system completely to user-space, leaving in the kernel only the > > code to access the hardware. > > I understand. > > Another question, if a machine has multiple audio devices, including an > USB one, can't we assume that we should try this one first? Or could a > choice mechanism like kern.timecounter with default value could be > implemented to make this new feature work out of the box? Almost. The reverse attaching order seems to cover most practical use cases. If the user coonects a USB device, it's probably because he wants to use it, so it would make sense for it to be tried first. But a choice mechanism seems necessary in certain cases. For instance "pro" audio interfaces need to be handled separately; certain USB devices (ex. webcams) come with an unused audio interface that the user may want to skip. > > Finally, is there any downside of having a smaller DEFAULT_ROUND? No. FWIW, the large default block and buffer sizes used to be necessary 10+ years ago because MP kernels could delay interrupts for more than a audio block which used to confuse azalia(4) and envy(4) permanently. Now drivers are resilient to interrupt loss and any block size could be used. Recent MP kernels behave better.
Re: switching between audio devices with no playback interruption
On 16/09/19(Mon) 19:31, Alexandre Ratchov wrote: > On Sun, Sep 15, 2019 at 03:01:36PM -0300, Martin Pieuchot wrote: > > On 29/08/19(Thu) 10:52, Alexandre Ratchov wrote: > > > The diff below allows to specifiy "an alternate device" to use in case > > > the audio device is disconnected. If so, programs continue playing > > > using the other device. > > > > > > For instance, if dmesg contains: > > > > > > audio0 at azalia0 > > > audio1 at uaudio0 > > > > > > and /etc/rc.conf.local contains: > > > > > > sndiod_flags=-f rsnd/0 -F rsnd/1 > > > > > > then sndiod will try to use the usb device by default. If it's > > > disconnected, programs continue using the internal one. Note that > > > there's no way to detect that the usb device is reconnected; > > > > Instead of doing a open/reopen dance in userland, which is inherently > > racy, did you consider introducing a driver abstracting the various > > audio(4) drivers attached? I'm thinking of something like wsmux(4) for > > audio. This should allow us to transparently attach/detach usb audio > > devices. > > I don't see any race conditions: once a device disappears, it is > closed and another one is opened in place. This only relies on calls > to open() and close() which are not racy. Unlike with hotplug(4), > there's no concurency between multiple code-paths; it's all > sequencial. I understand the logic for closing an existing device, but what about plugging a new one? I understand that this is not the scope of the diff, I wondering if that would fit in the model you're suggesting. It is just a matter of sending SIGHUP, so I could fake that when uaudio(4) attaches for example? Regarding the race I was talking about, doesn't dev_reopen() close the current device before trying to open a new one, possibly finding an incompatible device or failing to open it? What happens in this case? > I've considered the wsmux-style device for audio. Multiple audio > devices can't be combined, so the audio mux device would be a switch > between the devices, which is roughly what the diff does. > > Switching between devices, when possible, is not trivial, because the > state of one device must be restored on another device. It may require > channel mapping and/or format conversions, in case the devices don't > have the same capabilities. We don't want such complicated code to run > with full kernel privileges and to have access to all the physical > memory. FWIW, this was one of the reasons for moving the audio > sub-system completely to user-space, leaving in the kernel only the > code to access the hardware. I understand. Another question, if a machine has multiple audio devices, including an USB one, can't we assume that we should try this one first? Or could a choice mechanism like kern.timecounter with default value could be implemented to make this new feature work out of the box? Finally, is there any downside of having a smaller DEFAULT_ROUND?
Re: switching between audio devices with no playback interruption
On Sun, Sep 15, 2019 at 03:01:36PM -0300, Martin Pieuchot wrote: > On 29/08/19(Thu) 10:52, Alexandre Ratchov wrote: > > The diff below allows to specifiy "an alternate device" to use in case > > the audio device is disconnected. If so, programs continue playing > > using the other device. > > > > For instance, if dmesg contains: > > > > audio0 at azalia0 > > audio1 at uaudio0 > > > > and /etc/rc.conf.local contains: > > > > sndiod_flags=-f rsnd/0 -F rsnd/1 > > > > then sndiod will try to use the usb device by default. If it's > > disconnected, programs continue using the internal one. Note that > > there's no way to detect that the usb device is reconnected; > > Instead of doing a open/reopen dance in userland, which is inherently > racy, did you consider introducing a driver abstracting the various > audio(4) drivers attached? I'm thinking of something like wsmux(4) for > audio. This should allow us to transparently attach/detach usb audio > devices. I don't see any race conditions: once a device disappears, it is closed and another one is opened in place. This only relies on calls to open() and close() which are not racy. Unlike with hotplug(4), there's no concurency between multiple code-paths; it's all sequencial. I've considered the wsmux-style device for audio. Multiple audio devices can't be combined, so the audio mux device would be a switch between the devices, which is roughly what the diff does. Switching between devices, when possible, is not trivial, because the state of one device must be restored on another device. It may require channel mapping and/or format conversions, in case the devices don't have the same capabilities. We don't want such complicated code to run with full kernel privileges and to have access to all the physical memory. FWIW, this was one of the reasons for moving the audio sub-system completely to user-space, leaving in the kernel only the code to access the hardware. Furthermore, putting such code in the kernel would add an extra audio software layer, while we've all the necessary bits in sndiod. IMO, if in the future something ends up difficult to implement in user-space I'm not against pulling parts of the code in the kernel, but for now this doesn't seem necessary. > > internal one will keep playing until all programs stop; then sndiod > > will retry the usb one again. To force sndiod to retry the usb one, > > send SIGHUP. > > I'm not a fan of having such hotplug logic in userland because it is by > default racy and opens a can of worm for being able to configure how / > when a driver becomes the default one. > The configuration logic is simple and well-defined: there's an ordered list of devices. Whenever a device disappears, it's closed and the first working device of the list is opend. Configuring this is a matter of providing the (oredered) device list; this would be necessary in kernel implementation as well. > > As we're at it, the same logic is applied to MIDI as well; it makes > > possible to swap instuments without restarting programs. > > > > For all this to work, both audio devices must support the same rate > > (48kHz by default) and the same block size (now 10ms by default). This > > requires the block size calculation to be fixed in the audio(9) layer, > > so this diff is needed as well: > > With the idea above you could attach all 'converted' audio drivers to > the mux ;) AFAIK, all drivers are now "fixed" (hacks remain, but they still work). Now, whether switching between the devices is possible depends only on the hardware and the device operating mode (which is not known at attach time, btw).
Re: switching between audio devices with no playback interruption
On 29/08/19(Thu) 10:52, Alexandre Ratchov wrote: > The diff below allows to specifiy "an alternate device" to use in case > the audio device is disconnected. If so, programs continue playing > using the other device. > > For instance, if dmesg contains: > > audio0 at azalia0 > audio1 at uaudio0 > > and /etc/rc.conf.local contains: > > sndiod_flags=-f rsnd/0 -F rsnd/1 > > then sndiod will try to use the usb device by default. If it's > disconnected, programs continue using the internal one. Note that > there's no way to detect that the usb device is reconnected; Instead of doing a open/reopen dance in userland, which is inherently racy, did you consider introducing a driver abstracting the various audio(4) drivers attached? I'm thinking of something like wsmux(4) for audio. This should allow us to transparently attach/detach usb audio devices. > the > internal one will keep playing until all programs stop; then sndiod > will retry the usb one again. To force sndiod to retry the usb one, > send SIGHUP. I'm not a fan of having such hotplug logic in userland because it is by default racy and opens a can of worm for being able to configure how / when a driver becomes the default one. > As we're at it, the same logic is applied to MIDI as well; it makes > possible to swap instuments without restarting programs. > > For all this to work, both audio devices must support the same rate > (48kHz by default) and the same block size (now 10ms by default). This > requires the block size calculation to be fixed in the audio(9) layer, > so this diff is needed as well: With the idea above you could attach all 'converted' audio drivers to the mux ;)
switching between audio devices with no playback interruption
The diff below allows to specifiy "an alternate device" to use in case the audio device is disconnected. If so, programs continue playing using the other device. For instance, if dmesg contains: audio0 at azalia0 audio1 at uaudio0 and /etc/rc.conf.local contains: sndiod_flags=-f rsnd/0 -F rsnd/1 then sndiod will try to use the usb device by default. If it's disconnected, programs continue using the internal one. Note that there's no way to detect that the usb device is reconnected; the internal one will keep playing until all programs stop; then sndiod will retry the usb one again. To force sndiod to retry the usb one, send SIGHUP. As we're at it, the same logic is applied to MIDI as well; it makes possible to swap instuments without restarting programs. For all this to work, both audio devices must support the same rate (48kHz by default) and the same block size (now 10ms by default). This requires the block size calculation to be fixed in the audio(9) layer, so this diff is needed as well: https://marc.info/?l=openbsd-tech&m=156610818325864 Please test even if you don't use this feature as this diff changes the default audio block size (not all USB host controllers support large transfers). OK? Index: dev.c === RCS file: /cvs/src/usr.bin/sndiod/dev.c,v retrieving revision 1.58 diff -u -p -u -p -r1.58 dev.c --- dev.c 29 Aug 2019 07:38:15 - 1.58 +++ dev.c 29 Aug 2019 07:52:08 - @@ -968,7 +968,8 @@ dev_new(char *path, struct aparams *par, return NULL; } d = xmalloc(sizeof(struct dev)); - d->path = xstrdup(path); + d->path_list = NULL; + namelist_add(&d->path_list, path); d->num = dev_sndnum++; d->opt_list = NULL; @@ -1173,6 +1174,94 @@ dev_close(struct dev *d) dev_close_do(d); } +/* + * Close the device, but attempt to migrate everything to a new sndio + * device. + */ +void +dev_reopen(struct dev *d) +{ + struct slot *s; + long long pos; + unsigned int mode, round, bufsz, rate, pstate; + int delta; + + /* not opened */ + if (d->pstate == DEV_CFG) + return; + + if (log_level >= 1) { + dev_log(d); + log_puts(": reopening device\n"); + } + + /* save state */ + mode = d->mode; + round = d->round; + bufsz = d->bufsz; + rate = d->rate; + delta = d->delta; + pstate = d->pstate; + + /* close device */ + dev_close_do(d); + + /* open device */ + if (!dev_open_do(d)) { + if (log_level >= 1) { + dev_log(d); + log_puts(": found no working alternate device\n"); + } + dev_exitall(d); + return; + } + + /* check if new parameters are compatible with old ones */ + if (d->mode != mode || + d->round != round || + d->bufsz != bufsz || + d->rate != rate) { + if (log_level >= 1) { + dev_log(d); + log_puts(": alternate device not compatible\n"); + } + dev_close(d); + return; + } + + /* +* adjust time positions, make anything go back delta ticks, so +* that the new device can start at zero +*/ + for (s = d->slot_list; s != NULL; s = s->next) { + pos = (long long)(d->round - delta) * s->round + s->delta_rem; + s->delta_rem = pos % d->round; + s->delta += pos / (int)d->round; + s->delta -= s->round; + if (log_level >= 2) { + slot_log(s); + log_puts(": adjusted: delta -> "); + log_puti(s->delta); + log_puts(", delta_rem -> "); + log_puti(s->delta_rem); + log_puts("\n"); + } + } + if (d->tstate == MMC_RUN) { + d->mtc.delta -= delta * MTC_SEC; + if (log_level >= 2) { + dev_log(d); + log_puts(": adjusted mtc: delta ->"); + log_puti(d->mtc.delta); + log_puts("\n"); + } + } + + /* start the device if needed */ + if (pstate == DEV_RUN) + dev_wakeup(d); +} + int dev_ref(struct dev *d) { @@ -1279,7 +1368,7 @@ dev_del(struct dev *d) } midi_del(d->midi); *p = d->next; - xfree(d->path); + namelist_clear(&d->path_list); xfree(d); } Index: dev.h === RCS file: /cvs/src/usr.bin/sndiod/dev.h,v retrieving revision 1.21 diff -u -p -u -p -r1.21 dev.h --- dev.h 12 Jul 2019 06:30:55 - 1.21 +++ dev.h