xenolightning / AudioSwitcher

.NET Library which facilitates interacting with Audio Devices on Windows
Microsoft Public License
246 stars 53 forks source link

NullReferenceException triggered from other process #43

Open ygoe opened 5 years ago

ygoe commented 5 years ago

Very strange behaviour here. I've written an app that can do different things based on the command line parameter.

  1. It can open my external speakers (S/PDIF) and play a short inaudible sound to prevent the speakers from turning off after some idle time.
  2. It can play sounds at an interval on the same external speakers.

I use this library to set the desired volume for each task.

Option 1 is started through the Windows task scheduler at every full hour.

I can start option 2 interactively. It's a console application that only plays a sound (with NAudio) every few minutes.

Now when the interactive program is running and the keep-alive tasks runs in the background, then the interactive program crashes with a NullReferenceException in AudioSwitcher.AudioApi.CoreAudio.CoreAudioSession.Dispose(), called from AudioSwitcher.AudioApi.CoreAudio.CoreAudioSession.Finalize(). That's all I get when I attach a Visual Studio debugger. The exception is not in my code so I can't see it. Also, the exception occurs in another process than was started.

What is it doing there that affects other processes using the same library?

Here's the keep-alive part:

Guid deviceGuid = DirectSoundOut.DSDEVID_DefaultPlayback;
foreach (var dev in DirectSoundOut.Devices)
{
    if (dev.Description.Contains(name))
        deviceGuid = dev.Guid;
}
var cac = new CoreAudioController();
caDevice = cac.GetDevice(deviceGuid);
if (caDevice == null)
    return false;
bool wasMuted = caDevice.IsMuted;
double previousVolume = caDevice.Volume;
if (wasMuted)
    caDevice.Mute(false);
caDevice.Volume = 5;

PlaySine(19000, 0.5, 0.5);   // with NAudio
Thread.Sleep(50);

caDevice.Volume = previousVolume;
if (wasMuted)
    caDevice.Mute(true);
caDevice.Dispose();

The interactive task doesn't do anything while it crashes. It has played a sound before but isn't currently playing when the keep-alive task starts. So it's not interacting with the library and sits in a Sleep call somewhere while it dies.

xenolightning commented 5 years ago

Interesting.

It doesn't actually do anything "across processes", but the session management part monitors audio sessions that start/stop/disconnect and acts accordingly. Sessions can be created from other processes, which is likely what is happening here.

I wonder if it's length of the sound that is causing an issue? It's almost like the session is created and disconnected all before it can be acted upon.

Are you able to put together a small sample app which highlights the issue?

Also, what version of the api are you using?

Cheers.

ygoe commented 5 years ago

Version of the API? The NuGet version is the latest stable. Let me look it up … 3.0.0.1 / 3.0.0.

I'll put together a small sample.

ygoe commented 5 years ago

Here's the demo app. Start it once from the command line with the argument "interval" and let it play a sound every few seconds. While it's doing that, start the same program a second time with the argument "keepalive". When the second one is finished and returns, the first crashes.

Note: You may need to change the device name "S/PDIF" in the code to a name of a sound playback device on your system.

AudioPlayer-demo.zip

roblarky commented 4 years ago

I'm getting the same error, which silently/suddenly crashes my application sometime later. The interaction is via user scripts, so the code is being executed through Microsoft ClearScript. These scripts are executed in separate threads from the UI thread, but within the same process.

I tested this last night and here's what happens. This script is executed in ClearScript (using V8 engine):

var mixer = sp.GetPlaybackMixer();
mixer.Volume = 37;
mixer.Dispose();

The method sp.GetPlaybackMixer() is bridged to this C# method:

public static CoreAudioDevice GetPlaybackMixer()
{
    CoreAudioDevice defaultPlaybackDevice = new CoreAudioController().DefaultPlaybackDevice;
    return defaultPlaybackDevice;
}

The function is successful, the volume is set to 37%.

I executed this at 10:40PM and went to bed. This morning, my app was no longer running. Checked the Event Viewer and a .NET Runtime exception was logged at 12:09 AM (1.5 hours later):

Application: StrokesPlus.net.exe
Framework Version: v4.0.30319
Description: The process was terminated due to an unhandled exception.
Exception Info: System.NullReferenceException
   at AudioSwitcher.AudioApi.CoreAudio.CoreAudioSession.Dispose()
   at AudioSwitcher.AudioApi.CoreAudio.CoreAudioSession.Finalize()
roblarky commented 4 years ago

Follow up:

I was able to create a scenario in which I could consistently reproduce the crash. Then I downloaded the repo containing version 4 and used that instead. I can no longer reproduce the issue using the same scenario. I will report back if it crashes again using the latest version.

pengowray commented 1 year ago

Not sure of the status of this project, but maybe someone would like some resolution on this issue.

I'm also getting a similar error, also with version 3.0.0.1 / 3.0.0. Can't be 100% sure it's the same (I haven't tried testing the zip above to check, but it sounds the same)

When you get a pesky NullReferenceException in a library these days, Visual Studio these days prompts you to decompile the DLL to see where it's coming from. So I managed to track it down.

Here's the decompiled code in CoreAudioSessionController.RefreshSessions():

sessionList.GetCount(out var count); // ❌ NullReferenceException 

Here's how it looks in the actual code in the repo:

int count;
enumerator.GetCount(out count);

but... in the current repo there's a null check right before this code, which must presumably fix the problem. I checked when that was added: Feb 2, 2016. Here's the commit where the null check was added (with the relevant code highlighted):

https://github.com/xenolightning/AudioSwitcher/commit/8439f402e7d6d811d03d3efb1130778639594038#diff-90a724a616c16d72dc02b9be7a7ae4ab26cc54f3165cc834033ab51dcb01d932R189-R193

It looks like 3.0.0.1 is from Jan 12, 2016, a little before this commit.

Maybe this fix could be backported and compiled into a 3.0.0.2 ?

I did briefly try upgrading to a 4.0.0-alpha release but the API had changed, breaking some code and I couldn't work it out or find docs, so went I back to 3.0. Guess I should have another go at working it out.

xenolightning commented 1 year ago

@pengowray If you want to fire a pull request for that - I can probably push a patch release to nuget for the 3.x release chain

pengowray commented 1 year ago

Thanks for the response, @xenolightning. I've made a patch – #62 . Hopefully it's enough to fix the issue.

As an aside, if it's stable enough, I'd also recommend making a release version of the 4.0 branch so it's not as hidden on nuget, so as to encourage new users onto that branch (and also updating its .net version compatibility if that's not too difficult).

Sangheilioz commented 8 months ago

@xenolightning was this ever pushed to nuget? I'm encountering what I expect is the same issue (it sounds like it's the same but like @pengowray I didn't grab the zip above) I'm using Latest stable 3.0.0 and see no other options that are more recent.

xenolightning commented 8 months ago

@Sangheilioz - yeah this was pushed and should be solved in 3.0.3