Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switching playback streams between devices seamlessly on windows #533

Open
monaghanwashere opened this issue Feb 26, 2021 · 44 comments
Open

Comments

@monaghanwashere
Copy link

Hi,

I'm wondering if it's possible to switch playback streams seamlessly between devices on Windows. For egs, say I have my headphones set as the default device, and I have an active OpenAL session running and outputting audio to this device. While this is happening, if I set my speakers as my default device, I would like the OpenAL audio output to switch to the speakers seamlessly. My current understanding is that, this isn't possible out of the box, and my 2 options are basically:

  1. To manually implement, in my app, a way to preserve all the OpenAL buffers, sources, playback positions, context etc. , and manually close the headphones device, open the speakers device, and re-create the entire OpenAL session at the preserved position.

  2. Alternatively, I could use some sort of virtual device to actually output my audio to, and in turn route the output of that virtual device to the windows audio device of my choosing. My question here is -- can I use the OpenAL loopback device for that purpose? i.e. always, by default, render all my audio output to the loopback device. Simultaneously, I would open the Windows default device (headphones) and, render the loopback output and pass that rendered output to an OpenAL buffer/source that would be playing out on the headphones device. This way, when the windows default device changes to the speakers mid-session, I would close the headphones device on OpenAL and open the speakers device instead (all this time while keeping the loopback device active and running), and simply continue to pass the loopback rendered audio output to the new (speakers) device instead. This would save me the trouble of implementing a system in my app that would have to manually preserve the buffers/sources/context of the OpenAL session at all times.

Does this make sense? Would # 2 be possible or am I missing something?

Thanks

@monaghanwashere
Copy link
Author

Anyone?

@kcat
Copy link
Owner

kcat commented Mar 3, 2021

Option 2 would be possible, but with caveats. There would be extra latency since there would be the source streaming buffer queue on top of the device buffer OpenAL writes to, and you can't really match the rendering format to the device format, since OpenAL doesn't expose the latter (aside from maybe detecting HRTF/headphones with the ALC_HRTF_SOFT extension).

You could use SDL for cross-platform audio output, with an OpenAL Soft loopback device to render/mix. Then to switch devices, close and open the SDL device and continue rendering with the same loopback device (first changing its format with alcResetDeviceSOFT if needed). Though that may have issues with surround sound (I don't think SDL works terribly great with anything more than stereo).

I do intend on adding a feature to be able to "reopen" a device, or move a playback device to another output. There's just currently some dependencies between the device handle and the output handler that would make it dangerous, which would need to be handled first.

@monaghanwashere
Copy link
Author

you can't really match the rendering format to the device format, since OpenAL doesn't expose the latter

hmm, not sure i understand this part. i would be passing the loopback output to openal anyway, so wouldn't this be identical to the situation where i'm not using loopback and loading audio assets into openal to play that may or may not match the device format? i.e. what's the difference between egs. specifying & loading a float 32 bit audio asset into openal (regular initialization w/ hardware device), and calling play on it, as opposed to rendering loopback output to float 32 bit and then passing that output to an openal streaming source on the hardware device context, while specifying it as float 32 bit? Isn't OpenAL going to be handling the matching of the audio format to the device format under the hood in both cases?

I do intend on adding a feature to be able to "reopen" a device, or move a playback device to another output

Any chance there's a very rough estimate of this timeline?

@kcat
Copy link
Owner

kcat commented Mar 5, 2021

hmm, not sure i understand this part. i would be passing the loopback output to openal anyway, so wouldn't this be identical to the situation where i'm not using loopback and loading audio assets into openal to play that may or may not match the device format?

I mean having the loopback device render to stereo, quad, 5.1, 71, or whatever depending on the actual output device. Ideally the loopback device would render to the output device configuration, and the openal playback device's streaming source would enable direct channels (to avoid extra panning virtualization on the channels of the final mix). The only way I can see to avoid that with an openal playback device is to render the loopback device to b-format and stream a b-format source on the playback device. Though that may have its own slight issues with stereo speakers and headphones (the positioning might not be quite as good).

OpenAL isn't really well designed for simple direct stream output.

Any chance there's a very rough estimate of this timeline?

Maybe I could get something that sort of works by early next week, perhaps. But there will almost certainly be issues with backends that directly access hardware, with hardware that can't open an output multiple times simultaneously. It's not a very typical setup, as most people will have a software mixer/server to handle multiple outputs on the same device, if not a device capable of hardware mixing, but they do exist.

@Swyter
Copy link

Swyter commented Mar 5, 2021

Suffering from the same conundrum here.

I have implemented just that; having the normal 3D audio processing in a loopback device and then a transient physical device with a single alBufferCallbackSOFT()'d buffer/source that simplifies things a lot and seems to reduce latency. The problem is that now that I bite the bullet I depend on both AL_SOFT_callback_buffer / AL_SOFTX_callback_buffer and ALC_SOFT_loopback (and AL_SOFT_map_buffer / AL_SOFTX_map_buffer for DirectSound-like streaming, but that's for another thing) tying the game to a narrow/recent set of OpenAL Soft versions, kind of counter-intuitive for an open standard.

Another thing I have also noticed is that changing the default output device without unplugging or touching the previous default doesn't really disconnect alcGetIntegerv(ALC_CONNECTED) and keeps chugging normally even when sound is being routed somewhere else. This can be reproduced on Windows by switching between Bluetooth headphones and HDMI sound output: I need to keep polling alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER) every few seconds to forcefully refresh the internal list and compare the first entry's string (the default device) to my previously saved value.

Once I'm notified of that I attempt to alcResetDeviceSOFT(), and if that fails I tear the previous device down and open a new callback'd physical device. Repeating the cycle.

I wish there was a cleaner way of doing this. Callback-based, if possible.

Reusing alEventCallbackSOFT() / AL_EVENT_TYPE_DISCONNECTED_SOFT (which I haven't managed to get to work yet) would be ideal. Most back-ends expose something like this directly. Maybe coupled with something like alcGetIntegerv(ALC_STILL_DEFAULT) as fallback. Or just invalidating/disconnecting the device when 'focus' is lost.

Ultimately, I think at least in extension-less/plain OpenAL the best way of doing things would be to have a virtual device that matches the format and layout at startup and retains them as devices and formats change underneath. You may have to restart the application to get 5.1 and if the opposite thing happens you'll get downmixed audio, but at least you don't get pure silence on random changes until restart, like now, which I think is worse in real-world usability terms.

Let me know what you think.

--
PS: I originally planned to use something like SDL2 for real output and hot-plugging, but it only supports plain stereo in practice. Ironically OpenAL Soft is better in every single way except hot-plugging. Hopefully this limitation can be sorted out somehow cleanly via extensions. I don't mind them. For all intents and purposes this implementation is OpenAL now. And well worth getting married to it as long as it works well. Until then, I'll hack my way through. ¯\_(ツ)_/¯

@monaghanwashere
Copy link
Author

openal playback device's streaming source would enable direct channels (to avoid extra panning virtualization on the channels of the final mix)

@kcat I'm confused why you're suggesting a b-stream format render to a b-stream format source. Can you explain what you mean by 'avoid extra panning virtualization' ? Currently, my hardware device format is stereo (I'm just using a laptop with headphones/speakers). So I've set up the loopback device to render to stereo, and the streaming source on the openal playback device is also stereo. Am I going to be losing some quality of spatialization this way? I thought stereo sources were untouched/not spatialized...

I have implemented just that; having the normal 3D audio processing in a loopback device and then a transient physical device with a single alBufferCallbackSOFT()'d buffer/source that simplifies things a lot and seems to reduce latency. The problem is that now that I bite the bullet I depend on both AL_SOFT_callback_buffer / AL_SOFTX_callback_buffer and ALC_SOFT_loopback (and AL_SOFT_map_buffer / AL_SOFTX_map_buffer for DirectSound-like streaming, but that's for another thing) tying the game to a narrow/recent set of OpenAL Soft versions, kind of counter-intuitive for an open standard.

@Swyter can you explain what alBufferCallbackSOFT does? I don't find a reference to this anywhere in the documentation (in fact same with AL_SOFT_callback_buffer / AL_SOFT_map_buffer ... seems like the only relevant google result is this blob here written by kcat)...

@Swyter
Copy link

Swyter commented Mar 5, 2021

Yeah, I had to do a bit of spelunking in the Git history and the various internal tools that use them.

  • With AL_SOFT_callback_buffer you can essentially stream random data into a single circular buffer and it will keep playing whatever you are feeding it, chunk by chunk, calling your function whenever is about to run out of juice. So it should have pretty decent latency. I think.
ALsizei pcAudioCallback(ALvoid *userptr, ALvoid *sampledata, ALsizei sizeinbytes)
{
    /* swy: render an arbitrary amount of samples from the loopback device,
            relay them to the physical/transient one via callback */
    alcRenderSamplesSOFT(alDevLoopback, sampledata, sizeinbytes / frameSize); return sizeinbytes;
}

/* swy: create a transient device to relay whatever the loopback device is playing.
        save the name at startup to detect changes in the default device via string
        compare, not ideal, but the OpenAL Soft doesn't seem to refresh this list
        on its own, and no AL_EVENT_TYPE_DISCONNECTED_SOFT thingies are fired */
        
alDevPhysicalName = (char *) alcGetString(NULL, ALC_DEFAULT_ALL_DEVICES_SPECIFIER);
alCtxPhysical     =          alcCreateContext(alDevPhysical, attrs);

alcMakeContextCurrent(alCtxPhysical); AL_ASSERT;

ALuint LoopbackBuffer = 0, LoopbackSource = 0;

alGenBuffers(1, &LoopbackBuffer);
alGenSources(1, &LoopbackSource);

alBufferCallbackSOFT(LoopbackBuffer, AL_FORMAT_..., 48000, pcAudioCallback, alDevPhysical, 0);

alSourcei (LoopbackSource, AL_BUFFER, LoopbackBuffer);

alSourcei (LoopbackSource, AL_SOURCE_RELATIVE, AL_TRUE); AL_ASSERT;
alSource3i(LoopbackSource, AL_POSITION, 0, 0, -1); AL_ASSERT;
alSourcei (LoopbackSource, AL_ROLLOFF_FACTOR, 0);
alSourcei (LoopbackSource, AL_LOOPING, AL_FALSE);

if (hasBypassExtension)
    alSourcei(LoopbackSource, AL_DIRECT_CHANNELS_SOFT, hasBypassRemixExtension ? AL_REMIX_UNMATCHED_SOFT : AL_DROP_UNMATCHED_SOFT); AL_ASSERT;

alSourcePlay(LoopbackSource);

alcMakeContextCurrent(alCtxLoopback); AL_ASSERT;
  • And @kcat uses AL_SOFT_map_buffer in his dsoal implementation (code here) to mirror the DirectSound way of streaming. That is, there's a chunk of memory with a read and a write pointer, the game writes to memory directly whatever it wants, at any point, even overwriting whatever is currently being read. Whenever you are ready, you tell it that (a portion of) the buffer memory has changed via alFlushMappedBufferSOFT(), which seems equivalent to the lock/unlock mechanic.

In the game I work on I originally tried to mirror this extension using plain OpenAL buffer-chaining and it was imperfect and extremely challenging, so I can see why AL_SOFT_map_buffer exists. The game can write arbitrary chunks in arbitrary places while resetting or changing the playback head. So you need a way of overlaying the overlapping "blocks" without crashing and burning, and glitching out.

Implementing a less abstracted ring buffer, where every bit can be flipped, with a more abstracted way of doing the same thing via a collection of in-flight, fixed-size buckets is like trying to do surgery with a rubber ducky.

Giving the implementation a correct approximation of what the current offset in the virtual "buffer" chain is wasn't very fun either. But I'm a pretty bad programmer.

@kcat
Copy link
Owner

kcat commented Mar 6, 2021

I'm confused why you're suggesting a b-stream format render to a b-stream format source. Can you explain what you mean by 'avoid extra panning virtualization' ?

When you render using, say, a 5.1 loopback device, then play a 5.1 source on the playback device, OpenAL will virtualize the source channels. Each channel will be given a relative position around the listener, where it'll be run through the usual panning algorithm to be positioned as best it can regardless of the playback device's actual output configuration. So if the user has a quad or 7.1 setup, each source channel will seem to come from the same general position (rather than 5.1's side channel being naively moved to quad's rear channel, which isn't in the same position). Or if the user has stereo output, the channels will be downmixed appropriately. Or if the user has HRTF enabled, it will create a virtual surround sound effect.

So not only will sources be panned virtually when rendering the mix with the loopback device, the loopback device's channels will be panned virtually when mixing with the playback device.

Using a B-Format loopback mix (and a B-Format source stream) can help. B-Format doesn't render discrete channels that need to be positioned, it instead renders a continuous 3D soundfield. A B-Format source decodes that soundfield to the given output speaker configuration. Internally, this is exactly what OpenAL Soft's playback devices do*. It mixes to B-Format, then decodes the B-Format mix to the user's speaker configuration. A B-Format loopback mix then avoids that last decode bit, and the B-Format source stream completes it, avoiding the extra virtualization while matching the user's output configuration.

  • Mostly. By default, HRTF actually bypasses the B-Format mix to more directly use response measurements. The B-Format mix can and does still work, fairly well, but it's not exactly the same. And stereo speaker output actually has a subtle positioning correction for stereo sources so that the channels pass through as-is by default. It'll still work, but stereo sounds may seem a bit more narrow on stereo output.

A stereo loopback mix with a stereo source playback should work out fine. But once you get into other things, like surround sound or HRTF, you'll have work ahead of you to get separate loopback and playback devices working optimally.

With AL_SOFT_callback_buffer you can essentially stream random data into a single circular buffer and it will keep playing whatever you are feeding it, chunk by chunk, calling your function whenever is about to run out of juice. So it should have pretty decent latency. I think.

More or less. The callback gets invoked in the mixer when the source needs some more samples to mix. It requests exactly as many samples as it needs when it needs them. There's one big caveat: since it runs in the mixer, the callback needs to be fast and real-time safe. It can't do any locking, I/O, blocking, waiting, or make any calls that could do these things (or otherwise take too much time and cause the audio update to miss its deadline). And technically speaking, alcRenderSamplesSOFT isn't safe; it has to lock a mutex to verify the given device handle is valid. Although not all backends will be as sensitive to unsafe calls, so it may not be a problem (one of the reasons I've been so hesitant with adding the extension up to recently is I have no way to guarantee a callback is safe, so it could still work for most people despite doing "bad" things, and only start failing for people that happen to use a different backend).

I've been thinking of relaxing that about alcRenderSamplesSOFT, to just assume the device is valid and let the app crash or UB if it's not, so that it would be real-time safe. The idea of using a loopback device to do a submix that's fed into a source with a callback buffer for low-latency throughput is an obvious use-case that should be supported in some way.

@Swyter
Copy link

Swyter commented Mar 6, 2021

Thanks a lot for the explanation, that cleared up a few things I was wondering about. I'll give b-format a go.

What do you think about the idea of a virtual device that wraps all this complexity in a less roundabout way? I think it could simplify hot-plugging for most non-professional-audio users that can't tear their long-running 3D context down.

I call ALC_DEFAULT_ALL_DEVICES_SPECIFIER every eight seconds or so because doing a global rescan doesn't seem cheap, I don't know if there's a better way of detecting changes in the default device, or a way of knowing when our audio sink has lost 'focus'.

I'm considering using something lightweight and unbuffered like libsoundio for the final part.

@Swyter
Copy link

Swyter commented Mar 6, 2021

Side note: funnily enough even Minecraft seems to have this problem: https://bugs.mojang.com/browse/MC-44055

@kcat
Copy link
Owner

kcat commented Mar 7, 2021

What do you think about the idea of a virtual device that wraps all this complexity in a less roundabout way? I think it could simplify hot-plugging for most non-professional-audio users that can't tear their long-running 3D context down.

If it's done at the system level, where the app isn't responsible for moving its output around to different devices, or have to worry about reconfiguring its output, that's more or less fine. As long as you're okay with the fact that the OpenAL device configuration won't update with the move (e.g. if you move from a stereo device to surround sound, OpenAL Soft will continue mixing stereo, and if it moves to headphones, it won't start using HRTF automatically).

The main issue with doing virtual devices in OpenAL Soft itself basically boils down to the device needing to be reconfigured for the new output, as there's no guarantee a device it opens will be able to match what it's currently using. And the device can't/shouldn't asynchronously reconfigure itself (an app should be reasonably assured things like the HRTF status or the refresh/update rate won't randomly change while in use). This is partly why alcResetDeviceSOFT was added with some extensions, to give a clear point where these things can change, aside from the standard alcCreateContext.

Probably what I'll do is add some kind of messaging log/queue that can be queried with alcGetIntegerv to get a list of events that have happened (like 'device added', 'device removed', 'default changed'). Although whether or not this will work will depend on whether the backend API is capable of reporting such things. WASAPI and PulseAudio can I believe, but I'm not sure about JACK, OSS, etc.

@monaghanwashere
Copy link
Author

@Swyter how do you get access to alBufferCallbackSOFT ? I'm not seeing it available/exposed in any headers...

@kcat
Copy link
Owner

kcat commented Mar 9, 2021

@Swyter how do you get access to alBufferCallbackSOFT ? I'm not seeing it available/exposed in any headers...

It's an in-progress extension right now, subject to incompatible changes, so it's not made public. The current definitions need to be copied out of alc/inprogext.h and into your source (and use the standard alGetProcAddress function to get new functions), but this should only be done for testing and feedback purposes, and should not be done in production/release code since future library updates may silently change it and break existing code.

Once the extension is finalized, its definitions will be moved to the public alext.h header, and it can safely be used without risk of future breakages.

@kcat
Copy link
Owner

kcat commented Mar 9, 2021

There's a function/in-progress extension now with commit e824c80 for reopening a playback device.

if(alcIsExtensionPresent(device, "ALC_SOFTX_reopen_device"))
{
    ALCboolean (ALC_APIENTRY*alcReopenDeviceSOFT)(ALCdevice *device, const ALCchar *name, const ALCint *attribs);
    alcReopenDeviceSOFT = alcGetProcAddress(device, "alcReopenDeviceSOFT");

    success = alcReopenDeviceSOFT(device, new_device_name, attribs);
}

new_device_name is the device name to open and move output to (essentially the same as you'd have passed to alcOpenDevice), and attribs is the attributes to use with the new device (same as you'd pass to alcCreateContext or alcResetDeviceSOFT).

If the call returns ALC_TRUE, the output was moved to the new device and reset with the given attributes. If the call returns ALC_FALSE, the new device couldn't be opened (and an ALC error will be generated), but it should keep running on the device it was already on.

Be aware that this is an in-progress extension (as noted by the SOFTX in the extension name), so there can be potential API/ABI breaks in the future. However, feedback is very welcomed, if there's any changes you'd like to see.

There isn't yet a way to determine if the default device changed or anything, although as an alternative, you can use WASAPI directly just to get notifications for devices being added/removed and the default device being changed, and use OpenAL's reopen device function as needed in response to it. Obviously that will only work on Windows, but it's an option until I get something more cross-platform.

@Swyter
Copy link

Swyter commented Mar 9, 2021

This is fantastic, thank you very much! Makes sense. I'll give it a go.

I understand the complexity of having to provide a cross platform way of getting change notifications efficiently, when half of the backends are pretty simple and may not map well, or the solution needs a bunch of workarounds.

--

Having an event queue and expanding the AL_EVENT_TYPE_DISCONNECTED_SOFT angle of things (while still allowing polling in the traditional way via integer check) seems like an efficient way of exposing it. I think.

Just eventually having AL_EVENT_TYPE_DEFAULT_CHANGED_SOFT would make me a happy camper and probably would solve most of the issue for most users. Until then, I can poll on my end by forcing backend list refreshes and comparing strings. Five to ten seconds of silence is an acceptable delay when switching audio sinks, I think. It sure beats having to restart the game.

That and virtual devices aside, I appreciate the work you are single-handedly doing by keeping OpenAL relevant and modernizing a few blind spots that (understandably) haven't aged well.

After using it out of the box in four different platforms (and doing my own backend on Nintendo Switch) it has always been dependable and rock-solid.

So no complaints, I know what I'm going to get.

@Swyter
Copy link

Swyter commented Mar 16, 2021

Small note: For those reading this thread, and for future reference; keep in mind that profiling these ALC_ALL_DEVICES_SPECIFIER calls on Windows shows that device enumeration on a back-end like WASAPI (via CDeviceEnumerator::EnumAudioEndpoints() and CDeviceEnumerator::GetDefaultEndpoints(), see probe_devices() in alc/backends/wasapi.cpp and ProbeAllDevicesList() in alc.cpp) is a blocking call that can take at least 3 or 4 milliseconds of your frame on a beefy computer.

So, careful with your alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER); in a main thread. CPU profiling is your friend.

@kcat
Copy link
Owner

kcat commented Mar 16, 2021

that can take at least 3 or 4 milliseconds of your frame on a beefy computer.

Interesting, I'm surprised it's that fast. It has to marshal the call to a separate message handler thread (because Windows can't make it easy to call WASAPI methods on a whatever thread a caller happens to be on because of COM restrictions), and then has to wait for the message handler thread to wake up and do the actual enumeration before signaling that the enumeration is complete, before the calling thread can wake back up and report back with what it found. I'd have expected the scheduler to take a bit more time to wake up the handler thread to handle the enumeration, and to wake the calling thread back up after enumeration finished.

@monaghanwashere
Copy link
Author

Thanks for the quick turnaround on this kcat! I've been trying this extension, and it only seems to work sporadically. Often times, the audio stream is not moved (even though alcReopenDeviceSOFT will return ALC_TRUE). I see the following output on the console whenever it fails:
[ALSOFT] (EE) Failed to get padding: 0x88890004

Is there any more info I can provide to help with debugging?

@kcat
Copy link
Owner

kcat commented Mar 16, 2021

From what I can see, 0x88890004 is AUDCLNT_E_DEVICE_INVALIDATED, which MSDN says

IAudioClient::GetCurrentPadding
AUDCLNT_E_DEVICE_INVALIDATED - The audio endpoint device has been unplugged, or the audio hardware or associated hardware resources have been reconfigured, disabled, removed, or otherwise made unavailable for use.

When you say "the audio stream is not moved", do you mean it keeps playing on the same device, or outputs stops completely? Does it play at all on the new device? Since alcReopenDeviceSOFT returned ALC_TRUE, that tells me it successfully opened the new device, closed the old one, then configured and started playback on the new one, but just as OpenAL Soft went to start mixing and feeding it audio, it got an error for some reason. Maybe something to do with configuring and starting a new device so soon after the old one closed caused the underlying device to drop out just as it was getting started, though since both use shared-mode streams, I don't see why that would happen.

If you wait a second or so and call alcResetDeviceSOFT, does playback resume on the new device?

@monaghanwashere
Copy link
Author

monaghanwashere commented Mar 17, 2021

Ok first let me clarify my process, which is as follows: I have my headphones plugged into my laptop. I fire up my audio engine (which uses OpenAL), and verify I can hear audio playing through my headphones as usual. Then, I unplug my headphones. I expect the audio output to now continue through the laptop speakers. This happens around 25% of the time; most of the time I get that padding error in the console, and nothing plays through the speakers (even though alcReopenDeviceSOFT returned ALC_TRUE).

I'm getting the notification of the headphones being unplugged from the Windows WASAPI layer btw, and using that to trigger the alcReopenDeviceSOFT call.

When you say "the audio stream is not moved", do you mean it keeps playing on the same device, or outputs stops completely?

I can't answer the part of 'does it keep playing on the same device`, because the headphones are unplugged, so I'm not sure how to verify that. And as mentioned above, output stops completely i.e. i don't hear anything on the speakers.

If you wait a second or so and call alcResetDeviceSOFT, does playback resume on the new device?

This seems to make things a bit better i.e. the speakers play the audio a bit more reliably, maybe 75% of the time now, but it's still not consistent (I wait 2 seconds before calling alcResetDeviceSOFT).

EDIT: Interestingly enough, if I don't wait for 2 seconds and just call alcResetDeviceSOFT immediately after alcReopenDeviceSOFT, it seems to work 100% of the time...

@kcat
Copy link
Owner

kcat commented Mar 18, 2021

most of the time I get that padding error in the console, and nothing plays through the speakers (even though alcReopenDeviceSOFT returned ALC_TRUE).

I'm getting the notification of the headphones being unplugged from the Windows WASAPI layer btw, and using that to trigger the alcReopenDeviceSOFT call.

Are you also able to get notifications about devices being reconfigured? It sounds like after the headphones are unplugged, the ALCdevice is reopened to the speaker device (successfully), then just as it starts feeding it audio, Windows reconfigures the speaker device and drops all active output (which disconnects the ALCdevice, and alcResetDeviceSOFT should fix).

EDIT: Interestingly enough, if I don't wait for 2 seconds and just call alcResetDeviceSOFT immediately after alcReopenDeviceSOFT, it seems to work 100% of the time...

That is really odd. Just to be sure, when you get the padding error, the ALCdevice becomes disconnected, right? And the audio plays on the new device for as long as it doesn't become disconnected? And after it becomes disconnected, alcResetDeviceSOFT may or may not restore playback?

Either way, I made a slight tweak with commit 743f093 that might, unlikely but possibly, work better at reopening a device. It would be helpful if you can test.

@monaghanwashere
Copy link
Author

sorry for the extended pause here; i got pulled into another project.

so i tested out the latest version of openal-soft and unfortunately the problem still doesn't seem to have gone away completely i.e. I will occasionally get the padding error. It seems like it's happening less often now, but it's hard to say that definitively.

Are you also able to get notifications about devices being reconfigured?

Not sure what you mean by reconfigured per se, but I have a device monitor service that will fire a notification when a device is added, removed, or the default device has changed.

Just to be sure, when you get the padding error, the ALCdevice becomes disconnected, right?

How can I check to see if the ALCdevice has become disconnected?

And the audio plays on the new device for as long as it doesn't become disconnected?

The audio does not play on the new device when I get the padding error. So if I had headphones plugged in (audio playing through them fine) and I unplug them and get the padding error, the audio does not play on the speakers ("new" device) at all.

And after it becomes disconnected, alcResetDeviceSOFT may or may not restore playback?

Yes, this is correct. It seems I spoke too soon earlier when I said that calling alcResetDeviceSOFT immediately after alcReopenDeviceSOFT fixes the error 100% of the time; I was able to reproduce the padding error just now with both calls in place...

Please let me know how else I can continue to help testing, thanks

@kcat
Copy link
Owner

kcat commented Apr 17, 2021

Not sure what you mean by reconfigured per se, but I have a device monitor service that will fire a notification when a device is added, removed, or the default device has changed.

I mean if the device changes its format or period size, or switches between enabled and disabled (if that's different from added/removed), and things like that.

How can I check to see if the ALCdevice has become disconnected?

You can either use the ALC_EXT_disconnect extension to query alcGetIntegerv(device, ALC_CONNECTED, 1, &connected);, or the AL_SOFT_events extension to register a callback to watch for AL_EVENT_TYPE_DISCONNECTED_SOFT events.

The audio does not play on the new device when I get the padding error. So if I had headphones plugged in (audio playing through them fine) and I unplug them and get the padding error, the audio does not play on the speakers ("new" device) at all.

Okay, so it never gets a chance to start every time the error happens. The error is never a delayed occurrence. How about if, after calling alcReopenDeviceSOFT where it returns ALC_TRUE but causes the padding error, you wait 5 or 10 seconds (rather than just 2) after getting the padding/disconnection error to call alcResetDeviceSOFT, does it work then? If not, what about 20 or 30 seconds? Just trying to figure out if there's some timing and contention problem with the system causing the device to be down for a bit unexpectedly, or if I'm handling the device in a way that's preventing it from restarting properly no matter how long it's left alone.

@monaghanwashere
Copy link
Author

How about if, after calling alcReopenDeviceSOFT where it returns ALC_TRUE but causes the padding error, you wait 5 or 10 seconds (rather than just 2) after getting the padding/disconnection error to call alcResetDeviceSOFT, does it work then? If not, what about 20 or 30 seconds? Just trying to figure out if there's some timing and contention problem with the system causing the device to be down for a bit unexpectedly, or if I'm handling the device in a way that's preventing it from restarting properly no matter how long it's left alone.

Just tried this, and waiting for 5/10/20/30 seconds does NOT make a difference. When I hit the padding error, it doesn't matter how long I leave the device alone before calling alcResetDeviceSOFT. Audio never plays on the new device.

Just to be sure, when you get the padding error, the ALCdevice becomes disconnected, right?

I can now confirm that yes, the device is detected as having been disconnected. I did alcGetIntegerv(device, ALC_CONNECTED, 1, &connected) as you suggested, and the connected value is 0.

To re-iterate the sequence of events in the error case:

  1. Headphones are plugged in prior to launching app/starting OpenAL.
  2. Audio plays fine through headphones. I then unplug the headphones.
  3. I hit the padding error
  4. I check ALC_CONNECTED on the device that was just disconnected (headphones) and it returns 0
  5. I do the alcReopenDeviceSOFT call anyway (with the new device), it returns ALC_TRUE, after which I wait ~30+ seconds before calling alcResetDeviceSOFT on the new device.
  6. Audio does NOT play on the speakers after all of this.

Note that, however, when I don't get the padding error (i.e. and when audio is successfully transferred to the speakers), ALC_CONNECTED returns 1 in step 4. above i.e. the old device is still detected as being connected. In this success case, the waiting for 30s followed by the alcResetDeviceSoft call seems unnecessary; the audio plays on the speakers immediately, and after 30s I just hear a tiny hiccup in the audio, presumably signifying the execution of the alcResetDeviceSOFT call.

@kcat
Copy link
Owner

kcat commented Apr 20, 2021

  1. Headphones are plugged in prior to launching app/starting OpenAL.
  2. Audio plays fine through headphones. I then unplug the headphones.
  3. I hit the padding error
  4. I check ALC_CONNECTED on the device that was just disconnected (headphones) and it returns 0
  5. I do the alcReopenDeviceSOFT call anyway (with the new device), it returns ALC_TRUE, after which I wait ~30+ seconds before calling alcResetDeviceSOFT on the new device.
  6. Audio does NOT play on the speakers after all of this.

That final alcResetDeviceSOFT is only necessary as an attempt to restart the device if alcReopenDeviceSOFT succeeds in opening the new device, but fails/disconnects when trying to actually feed it samples. It's not needed if it successfully reopens and plays on a new device.

Getting a padding error just as the headphones unplug is expected. The audio device becomes disabled, which reasonably causes an AUDCLNT_E_DEVICE_INVALIDATED system error and OpenAL Soft then flags the device as disconnected. And actually, there should always be a "Failed to get padding..." or "Failed to buffer data..." error when the device disconnects (depending on where processing happens to be when the device is lost), since the mixer thread will only ever flag the device as disconnected after logging one of those messages.

At this point, callingalcReopenDeviceSOFT clears the disconnected flag, and tries to reopen, reset, and restart the device. It's what happens at this point that I'm most interested in... since the function returns ALC_TRUE, that means all the system calls OpenAL Soft made succeeded and everything seemed to start fine. You can query the device's ALC_ALL_DEVICES_SPECIFIER string to verify it moved to a valid device, and you can query its ALC_CONNECTED status to see whether it became disconnected again despite successfully reopening.

Are you able to provide a full trace log of a run where audio failed to come back on after unplugging the device? Either wherever the output of OutputDebugStringW goes, or by setting the ALSOFT_LOGLEVEL environment variable to 3 and watching stderr.

@monaghanwashere
Copy link
Author

Here's the log using ALSOFT_LOGLEVEL at 3 for when the audio fails to come back on after unplugging the device:

62: [ALSOFT] (II) Initializing library v1.21.1-92148a3a master
63: [ALSOFT] (II) Supported backends: wasapi, dsound, winmm, null, wave
68: [ALSOFT] (II) Loading config C:\Users\Elise.Monaghan\AppData\Roaming\alsoft.ini...
69: [ALSOFT] (II) Got binary: C:\work_my_branch\bin\Debug, unit-tests.exe
70: [ALSOFT] (II) Loading config C:\work_my_branch\bin\Debug\alsoft.ini...
71: [ALSOFT] (II) Key disable-cpu-exts not found
72: [ALSOFT] (II) Vendor ID: ""
73: [ALSOFT] (II) Name: "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"
74: [ALSOFT] (II) Extensions: +SSE +SSE2 +SSE3 +SSE4.1
75: [ALSOFT] (II) Key rt-prio not found
76: [ALSOFT] (II) Key resampler not found
77: [ALSOFT] (II) Key trap-al-error not found
78: [ALSOFT] (II) Key trap-alc-error not found
79: [ALSOFT] (II) Key reverb/boost not found
80: [ALSOFT] (II) Key drivers not found
81: [ALSOFT] (II) Starting message thread
90: [ALSOFT] (II) Message thread initialization complete
91: [ALSOFT] (II) Starting message loop
92: [ALSOFT] (II) Initialized backend "wasapi"
93: [ALSOFT] (II) Added "wasapi" for playback
94: [ALSOFT] (II) Added "wasapi" for capture
95: [ALSOFT] (II) Key excludefx not found
96: [ALSOFT] (II) Key default-reverb not found
97: [ALSOFT] (II) Got message "Enumerate Playback" (0x0006, this=0000000000000000, param=0000000000000000)
101: [ALSOFT] (II) Got device "OpenAL Soft on HeadPhone/Digital Output (Realtek High Definition Audio)", "{6046919D-C113-4482-B959-80E9FE9D600C}", "{0.0.0.00000000}.{6046919d-c113-4482-b959-80e9fe9d600c}"
102: [ALSOFT] (II) Got device "OpenAL Soft on Speakers (Realtek High Definition Audio)", "{65819B1B-55EF-4CC4-81DA-A3275E8CF42E}", "{0.0.0.00000000}.{65819b1b-55ef-4cc4-81da-a3275e8cf42e}"
103: [ALSOFT] (II) Got message "Open Device" (0x0000, this=0000019209370568, param=0000000000000000)
104: [ALSOFT] (II) Key channels not found
105: [ALSOFT] (II) Key sample-type not found
106: [ALSOFT] (II) Key frequency not found
107: [ALSOFT] (II) Key period_size not found
108: [ALSOFT] (II) Key periods not found
109: [ALSOFT] (II) Key sources not found
110: [ALSOFT] (II) Key slots not found
111: [ALSOFT] (II) Key sends not found
112: [ALSOFT] (II) Key ambi-format not found
113: [ALSOFT] (II) Created device 000001920E3B2020, "OpenAL Soft on HeadPhone/Digital Output (Realtek High Definition Audio)"
116: [ALSOFT] (II) ALC_MONO_SOURCES = 504
117: [ALSOFT] (II) ALC_STEREO_SOURCES = 7
118: [ALSOFT] (II) ALC_MAX_AUXILIARY_SENDS = 4
119: [ALSOFT] (II) ALC_HRTF_SOFT = 1
120: [ALSOFT] (II) Key frequency not found
121: [ALSOFT] (II) Key period_size not found
122: [ALSOFT] (II) Key periods not found
123: [ALSOFT] (II) Key sources not found
124: [ALSOFT] (II) Key sends not found
125: [ALSOFT] (II) Key hrtf not found
126: [ALSOFT] (II) Pre-reset: *Stereo, Float32, 44100hz, 882 / 2646 buffer
127: [ALSOFT] (II) Got message "Reset Device" (0x0002, this=0000019209370568, param=0000000000000000)
133: [ALSOFT] (II) Requesting playback format:
144: [ALSOFT] (II) Post-reset: Stereo, Float32, 96000hz, 1920 / 5760 buffer
145: [ALSOFT] (II) Key stereo-mode not found
146: [ALSOFT] (II) Key hrtf-paths not found
147: [ALSOFT] (II) Searching C:\work\cpp-engine-unit-tests*.mhr
148: [ALSOFT] (II) Searching C:\Users\Elise.Monaghan\AppData\Roaming\openal\hrtf*.mhr
150: [ALSOFT] (II) Searching C:\ProgramData\openal\hrtf*.mhr
151: [ALSOFT] (II) Adding built-in entry "!1_Built-In HRTF"
152: [ALSOFT] (II) Key default-hrtf not found
153: [ALSOFT] (II) Loading !1_Built-In HRTF...
154: [ALSOFT] (II) Detected data set format v3
155: [ALSOFT] (II) Resampling HRTF Built-In HRTF (44100hz -> 96000hz)
156: [ALSOFT] (WW) Resampled delay exceeds max (98.00 > 63)
157: [ALSOFT] (II) Loaded HRTF Built-In HRTF for sample rate 96000hz, 70-sample filter
158: [ALSOFT] (II) Key hrtf-size not found
159: [ALSOFT] (II) Key hrtf-mode not found
160: [ALSOFT] (II) 1st order + Full HRTF rendering enabled, using "Built-In HRTF"
161: [ALSOFT] (II) Channel config, Main: 4, Real: 2
162: [ALSOFT] (II) Allocating 6 channels, 24576 bytes
163: [ALSOFT] (II) Min delay: 10.25, max delay: 42.00, FIR length: 70
164: [ALSOFT] (II) New max delay: 31.75, FIR length: 102
165: [ALSOFT] (II) Key decoder/nfc not found
166: [ALSOFT] (II) Max sources: 511 (504 + 7), effect slots: 64, sends: 4
167: [ALSOFT] (II) Key dither not found
168: [ALSOFT] (II) Key dither-depth not found
169: [ALSOFT] (II) Dithering disabled
170: [ALSOFT] (II) Key output-limiter not found
171: [ALSOFT] (II) Output limiter disabled
172: [ALSOFT] (II) Fixed device latency: 2666666ns
173: [ALSOFT] (II) Got message "Start Device" (0x0003, this=0000019209370568, param=0000000000000000)
174: [ALSOFT] (II) Increasing allocated voices to 256
175: [ALSOFT] (II) Key volume-adjust not found
176: [ALSOFT] (II) Created context 000001920ECF71A0
294: [ALSOFT] (EE) Failed to get padding: 0x88890004
296: [ALSOFT] (II) Got message "Enumerate Playback" (0x0006, this=0000000000000000, param=0000000000000000)
297: [ALSOFT] (II) Got device "OpenAL Soft on Speakers (Realtek High Definition Audio)", "{65819B1B-55EF-4CC4-81DA-A3275E8CF42E}", "{0.0.0.00000000}.{65819b1b-55ef-4cc4-81da-a3275e8cf42e}"
300: [ALSOFT] (II) Got message "Stop Device" (0x0004, this=0000019209370568, param=0000000000000000)
301: [ALSOFT] (II) Got message "Reopen Device" (0x0001, this=0000019209370568, param=000001920D7C65A0)
302: [ALSOFT] (II) Reopened device 000001920E3B2020, "OpenAL Soft on Speakers (Realtek High Definition Audio)"
303: [ALSOFT] (II) Increasing allocated voices to 256
304: [ALSOFT] (II) ALC_MONO_SOURCES = 504
305: [ALSOFT] (II) ALC_STEREO_SOURCES = 7
306: [ALSOFT] (II) ALC_MAX_AUXILIARY_SENDS = 4
307: [ALSOFT] (II) ALC_HRTF_SOFT = 1
308: [ALSOFT] (II) Key frequency not found
309: [ALSOFT] (II) Key period_size not found
310: [ALSOFT] (II) Key periods not found
311: [ALSOFT] (II) Key sources not found
312: [ALSOFT] (II) Key sends not found
313: [ALSOFT] (II) Key hrtf not found
314: [ALSOFT] (II) Pre-reset: *Stereo, Float32, 44100hz, 882 / 2646 buffer
315: [ALSOFT] (II) Got message "Reset Device" (0x0002, this=0000019209370568, param=0000000000000000)
316: [ALSOFT] (II) Requesting playback format:
327: [ALSOFT] (II) Post-reset: Stereo, Float32, 48000hz, 960 / 2880 buffer
328: [ALSOFT] (II) Key stereo-mode not found
329: [ALSOFT] (II) Loading !1_Built-In HRTF...
330: [ALSOFT] (II) Detected data set format v3
331: [ALSOFT] (II) Resampling HRTF Built-In HRTF (44100hz -> 48000hz)
332: [ALSOFT] (II) Loaded HRTF Built-In HRTF for sample rate 48000hz, 35-sample filter
333: [ALSOFT] (II) HrtfStore 000001920CD8F4B0 decreasing refcount to 0
334: [ALSOFT] (II) Unloading unused HRTF !1_Built-In HRTF
335: [ALSOFT] (II) Key hrtf-size not found
336: [ALSOFT] (II) Key hrtf-mode not found
337: [ALSOFT] (II) 1st order + Full HRTF rendering enabled, using "Built-In HRTF"
338: [ALSOFT] (II) Channel config, Main: 4, Real: 2
339: [ALSOFT] (II) Allocating 6 channels, 24576 bytes
340: [ALSOFT] (II) Min delay: 8.00, max delay: 32.75, FIR length: 35
341: [ALSOFT] (II) New max delay: 24.75, FIR length: 60
342: [ALSOFT] (II) Key decoder/nfc not found
343: [ALSOFT] (II) Max sources: 511 (504 + 7), effect slots: 64, sends: 4
344: [ALSOFT] (II) Key dither not found
345: [ALSOFT] (II) Key dither-depth not found
346: [ALSOFT] (II) Dithering disabled
347: [ALSOFT] (II) Key output-limiter not found
348: [ALSOFT] (II) Output limiter disabled
349: [ALSOFT] (II) Fixed device latency: 5333333ns
350: [ALSOFT] (II) Got message "Start Device" (0x0003, this=0000019209370568, param=0000000000000000)

Let me know if you need any more info.

@kcat
Copy link
Owner

kcat commented Apr 21, 2021

If that's where the log ends until the device closes, everything looks fine. The new device is opened, configured, and started without error. Nor is there any error from the new mixing thread. Is the device still connected (as reported by ALC_CONNECTED) when it's silent? Do source states become AL_PLAYING when played and do their offsets properly update?

@monaghanwashere
Copy link
Author

monaghanwashere commented Apr 21, 2021

Is the device still connected (as reported by ALC_CONNECTED) when it's silent?

No, the device is not reported as connected when it's silent (ALC_CONNECTED is 0)

Do source states become AL_PLAYING when played and do their offsets properly update?

No, just checked, and the source states become AL_STOPPED when the padding error happens and there is silence.

I also got a trace log when I don't get the padding error, and it's identical to the one I posted above for the padding error, with the exception of this line, which is present in the padding error log above, but absent in the successful case log:

303: [ALSOFT] (II) Increasing allocated voices to 256

This line appears twice in the padding error log (once after Got message "Start Device" and once after Reopened device), but only once in the successful case log (only after Got message "Start Device").

Does that mean anything?

@kcat
Copy link
Owner

kcat commented Apr 22, 2021

No, the device is not reported as connected when it's silent (ALC_CONNECTED is 0)
...
No, just checked, and the source states become AL_STOPPED when the padding error happens and there is silence.

Just to make sure, this is the case after calling alcReopenDeviceSOFT? The device remains with ALC_CONNECTED = 0 and sources immediately become AL_STOPPED when you try to play them after it was reopened?

I also got a trace log when I don't get the padding error, and it's identical to the one I posted above for the padding error, with the exception of this line, which is present in the padding error log above, but absent in the successful case log:

303: [ALSOFT] (II) Increasing allocated voices to 256

This line appears twice in the padding error log (once after Got message "Start Device" and once after Reopened device), but only once in the successful case log (only after Got message "Start Device").

Does that mean anything?

That would suggest the device never became disconnected. Which would be odd if you don't reopen the device until the device disconnects. Are you able to make a test case that exhibits the problem? A minimal example with code that waits for the device to disconnect and reopens the device, showing that it's still disconnected afterward?

@monaghanwashere
Copy link
Author

Just to make sure, this is the case after calling alcReopenDeviceSOFT? The device remains with ALC_CONNECTED = 0 and sources immediately become AL_STOPPED when you try to play them after it was reopened?

No, this is the case before calling alcReopenDeviceSOFT (as I outlined in my steps here earlier; you'll see I query ALC_CONNECTED in step 4, and call alcReopenDeviceSOFT in step 5.).

I just tried querying ALC_CONNECTED after calling alcReopenDeviceSOFT (in the case where I hit the padding error), and it shows as connected i.e. it returns 1. So it seems, like you said, that indeed the device never becomes disconnected.

Also to address what you said here earlier:

Getting a padding error just as the headphones unplug is expected. The audio device becomes disabled, which reasonably causes an AUDCLNT_E_DEVICE_INVALIDATED system error and OpenAL Soft then flags the device as disconnected. And actually, there should always be a "Failed to get padding..." or "Failed to buffer data..." error when the device disconnects (depending on where processing happens to be when the device is lost), since the mixer thread will only ever flag the device as disconnected after logging one of those messages.

This is not what I'm experiencing though. I only get the padding error when hitting the silent audio case. Most of the times when I unplug my headphones, there are absolutely no errors (neither the padding nor the buffer data one).

Do you still want me to make an example with code? In what form would you like this? Should I just paste a minimal set of code here that reproduces the problem for me?

@kcat
Copy link
Owner

kcat commented Apr 23, 2021

No, this is the case before calling alcReopenDeviceSOFT (as I outlined in my steps here earlier; you'll see I query ALC_CONNECTED in step 4, and call alcReopenDeviceSOFT in step 5.).

Sure, but the problem you're having is that there's still no audio after reopening the device. There obviously won't and can't be any audio when the headphones are unplugged, which causes ALC_CONNECTED to become false and stop all sources, so that's expected and normal. But then after calling alcReopenDeviceSOFT, it should become connected again on a new device and allow you to play sounds again, so we need to figure out what's going on after reopening the device that causes it to still not play sounds.

I just tried querying ALC_CONNECTED after calling alcReopenDeviceSOFT (in the case where I hit the padding error), and it shows as connected i.e. it returns 1. So it seems, like you said, that indeed the device never becomes disconnected.

It's expected for the device to become disconnected when the headphones are unplugged (which is why the padding error shows up). The alcReopenDeviceSOFT call reconnects the device by opening a new output for it, so you should be able to play sounds again if that succeeds and the device is connected again.

I think you might be getting confused. When you physically unplug the headphones, Windows detects this and marks the system device as disabled, changes the default audio device as needed, and sends out a notification about the change. After that, the next time OpenAL Soft tries to access that system device it gets an AUDCLNT_E_DEVICE_INVALIDATED error, prints the padding/buffering error, then sets the ALCdevice's ALC_CONNECTED flag to ALC_FALSE (0) and stops all sources (this is the behavior defined by the ALC_EXT_disconnect extension, so an app waiting for a source to stop without expecting the device to be lost won't lockup). At this point, an application can query the ALCdevice's ALC_CONNECTED flag to see that it's ALC_FALSE (0), and try to reopen the device with a call to alcReopenDeviceSOFT. If that call succeeds, the ALC_CONNECTED flag will be set back to ALC_TRUE (1) and you can play sources again.

Because the sources were previously stopped when the headphones were disconnected, they won't be playing when it reopens/reconnects and it will stay silent if you don't play a source. If, when you do play a source now, it becomes AL_STOPPED immediately and remains silent despite ALC_CONNECTED being true, there's an issue. But I need to know if that's is what's happening.

@monaghanwashere
Copy link
Author

Before I post a detailed reply, let me clarify:

Because the sources were previously stopped when the headphones were disconnected, they won't be playing when it reopens/reconnects and it will stay silent if you don't play a source. If, when you do play a source now, it becomes AL_STOPPED immediately and remains silent despite ALC_CONNECTED being true, there's an issue. But I need to know if that's is what's happening.

Are you saying that, after calling alcReopenDeviceSOFT, that I additionally need to manually call alSourcePlay on all the stopped sources in order to resume playback on the new device?

@kcat
Copy link
Owner

kcat commented Apr 23, 2021

Are you saying that, after calling alcReopenDeviceSOFT, that I additionally need to manually call alSourcePlay on all the stopped sources in order to resume playback on the new device?

All stopped sources that should be playing, yes. You of course don't need to play sources that could've already stopped naturally, or sources that you don't want to start over from the beginning.

@monaghanwashere
Copy link
Author

monaghanwashere commented Apr 23, 2021

Okay, I think I see where our misunderstanding is then. The key lies in what you said here:

When you physically unplug the headphones, Windows detects this and marks the system device as disabled, changes the default audio device as needed, and sends out a notification about the change. After that, the next time OpenAL Soft tries to access that system device it gets an AUDCLNT_E_DEVICE_INVALIDATED error, prints the padding/buffering error, then sets the ALCdevice's ALC_CONNECTED flag to ALC_FALSE (0) and stops all sources (this is the behavior defined by the ALC_EXT_disconnect extension, so an app waiting for a source to stop without expecting the device to be lost won't lockup).

Specifically this part (which I'm assuming is asynchronous?):

the next time OpenAL Soft tries to access that system device

Ok, as you know, when I unplug the headphones, my windows device monitor service sends a notification about the device change, and that's when I invoke my callback that calls alcReopenDeviceSOFT. (Also note that all my sources are continuously playing, on loop, so I don't have any sources that naturally stop etc.)

Scenario I

Now: most of the times, when my callback gets invoked, it appears that OpenAL Soft has not yet tried to access that system device. This is evidenced by the fact that a) I don't get any padding error, and also b) at this point (before calling alcReopenDeviceSOFT), if I query ALC_CONNECTED, it shows the device as still being connected -- even though the headphones are unplugged. In this state, when I call alcReopenDeviceSOFT, the audio playback is moved seamlessly to the speakers, without the need for me to call alSourcePlay again on my previously playing sources. Makes sense, because OpenAL Soft didn't try to access the device yet, so it never got AUDCLNT_E_DEVICE_INVALIDATED, and never had to stop the sources. It just so happens timing-wise that this is the scenario that I encounter the majority of the time. This is what led me to believe that I don't need any manual alSourcePlay calls when invoking alcReopenDeviceSOFT (because everything 'just works' most of the times when I unplug my headphones).

Scenario II

Now, if "the next time OpenAL Soft tries to access that system device" happens before my callback is invoked, that's when I get the padding error. In this situation, if I query ALC_CONNECTED before calling alcReopenDeviceSOFT, it correctly shows the device as having been disconnected (reaffirming what the padding error signifies anyway), and like you said, proceeds to stop all the sources. Again, I didn't realize that I needed to manually re-invoke alSourcePlay after calling alcReopenDeviceSOFT, because this stopping of all the sources was not happening most of the times.

--

If, when you do play a source now, it becomes AL_STOPPED immediately and remains silent despite ALC_CONNECTED being true, there's an issue. But I need to know if that's is what's happening.

So nope, this isn't (and wasn't ever) happening (I was simply observing the sources were AL_STOPPED, which they weren't ever in Scenario I). I just tried manually calling alSourcePlay on the sources, and they indeed play correctly on the speakers. Everything makes sense now.

It sounds like, however, that in order to 'seamlessly' continue playback on the speakers -- i.e. have my sources simply pick up where they left off, and continue playback on the speakers from the offset they were at when they were playing back on the headphones -- I would need to continuously be caching what offset my sources are at in every instant. That way, when OpenAL Soft stops all of them, and I have to call alSourcePlay after re-opening the device, I can restore all the offsets first.

At this point I'm inclined to ask -- is it possible to have ALC_EXT_disconnect pause all the sources instead of stopping them when it detects a device disconnection? This would save the work of having to always keep track of all the offsets for all sources at all points in time. Also, is there anything else about the sources that OpenAL Soft changes other than it's playback state when a device is disconnected? i.e. are the gains or positions or any other properties of the sources reset?

Let me know if you have suggestions for how I can resume playback on the speakers at the last playback point in a way that has minimal overhead.

@kcat
Copy link
Owner

kcat commented Apr 24, 2021

At this point I'm inclined to ask -- is it possible to have ALC_EXT_disconnect pause all the sources instead of stopping them when it detects a device disconnection? This would save the work of having to always keep track of all the offsets for all sources at all points in time.

Not actually pause, no (there's too much of an assumption that sources can only ever stop asynchronously, and can only pause synchronously). But it might be possible to have them hold their play state, effectively pausing but still report as playing despite not actually doing so, and automatically start playing again when reconnected. It would have to be opt-in behavior, probably a context creation attribute or a context state, so an app would only set it when it knows it can't rely on a playing source to always be progressing.

Although it's notable that such a thing is only possible because OpenAL Soft does all the mixing itself. If processing were to ever be offloaded to an external process or device, and that process or device gets lost, then the current play state would be well and truly lost. I don't foresee OpenAL Soft ever doing that, but it's something to keep in mind regarding other potential implementations.

Also, is there anything else about the sources that OpenAL Soft changes other than it's playback state when a device is disconnected? i.e. are the gains or positions or any other properties of the sources reset?

No, all those properties remain as they were. It's just the play state (number of processed buffers and byte/sample/sec offset included) that's lost.

@monaghanwashere
Copy link
Author

monaghanwashere commented Apr 26, 2021

But it might be possible to have them hold their play state, effectively pausing but still report as playing despite not actually doing so, and automatically start playing again when reconnected. It would have to be opt-in behavior, probably a context creation attribute or a context state, so an app would only set it when it knows it can't rely on a playing source to always be progressing.

That would be very helpful. Any chance this is functionality that you plan on committing anytime soon?

Let me know if you have suggestions for how I can resume playback on the speakers at the last playback point in a way that has minimal overhead.

So is there any other recourse I have currently other than saving the offset positions continually and restoring them?

@kcat
Copy link
Owner

kcat commented Apr 26, 2021

That would be very helpful. Any chance this is functionality that you plan on committing anytime soon?

Likely some time very soon. Within they day, probably.

So is there any other recourse I have currently other than saving the offset positions continually and restoring them?

Not if you change the source's pitch or the source or listener velocity (which implicitly changes its pitch). If you don't, then if accuracy isn't much of a concern there is the option to just save the source's start time when you play it, then on disconnect, get the last known device time and use the difference. So when starting a source, something like:

alSourcePlay(source[i]);
// From ALC_SOFT_device_clock
alcGetInteger64v(device, ALC_DEVICE_CLOCK_SOFT, 1, &source_start[i]);

This gets an int64_t start time value in nanoseconds. That's all you need to do in normal operation, you don't need to continually query the current offset. When the device disconnects, then, you get the device clock time it stopped at, and the difference is how long a given source was playing for. Convert that to samples, and you have the sample offset it should roughly have been at.

alcGetIntegerv(device, ALC_CONNECTED, 1, &connected);
if(!connected)
{
    int64_t stop_time;
    alcGetInteger64v(device, ALC_DEVICE_CLOCK_SOFT, 1, &stop_time);

    ... reopen the device ...

    std::vector<ALuint> pending_sources;
    for(each source that should be playing)
    {
        int64_t play_time = stop_time - source_start[i];
        int sample_offset = (int)(playtime * source_srate / 1000000000);
        // Make sure to wrap sample_offset around the loop points if the source is looping.
        alSourcei(source[i], AL_SAMPLE_OFFSET, sample_offset);
        pending_sources.push_back(source[i]);
    }
    // Play all sources that should be playing at once
    alSourcePlayv(pending_sources.size(), pending_sources.data());
}

This will of course all be unnecessary if you don't mind waiting for the extension to hold the source state.

@monaghanwashere
Copy link
Author

Thanks for the writeup. Sure, I don't mind waiting for the extension update; please let me know when it becomes available.

@kcat
Copy link
Owner

kcat commented Apr 27, 2021

Commit 26c8c50 partially implements a new extension for this. Call alDisable(AL_STOP_SOURCES_ON_DISCONNECT_SOFT); to have it not stop sources on disconnect. There's a couple caveats: there may be some bugs if you change it and play/pause/stop/rewind a source after a device has already disconnected. It currently can't be reenabled after being disabled. There are some logistical problems relating to how OpenAL Soft handles playing/pausing/stopping/rewinding a source while a device becomes or is disconnected, and working out the expected behavior, that makes it tricky.

@monaghanwashere
Copy link
Author

Thanks! Going to try this out and will report back....

@maciejandrzejewski-digica

@kcat I'm using OpenAL v1.22.2 and extension ALC_SOFT_reopen_device nor ALC_SOFTX_reopen_device is available. Function alcIsExtensionPresent() returns false. Where I can find this extension?

@kcat
Copy link
Owner

kcat commented Mar 13, 2023

If you're using the router, you need to have an open device handle for an OpenAL Soft device to query and get the function with. A null device pointer will query the router itself, which doesn't handle the extension directly. Also, for it to work with the router, you need to use OpenAL Soft's router. Creative's router is designed in a way that doesn't allow the extension to work.

If you're not using the router, then double check you're using that version of OpenAL Soft (with a context set up, alGetVersion should say). The extension was added with 1.22.0, so it should exist in 1.22.2.

@maciejandrzejewski-digica

@kcat You are right. I got version mixed up with the system libraries v1.19. I have replaced system libraries with compiled one but now the device will not get opened. That is strange..

@maciejandrzejewski-digica

@kcat I have cleared out the problem with the version and the extension is found properly. I didn't tested it though as the device does not change when output sink is switched. I was switching between the speakers and the bluetooth headset. In v1.19 the device was connected with audio output and it changed when switching between the speakers and bluetooth headset. That is why I was searching a way to reuse the buffers in the new device and found out this thread. In v1.22 I see that OSS backend is used and changing the audio output sink does not change the device name anymore. So the problem is solved without the need to use the extension.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants