AudioSwitcher icon indicating copy to clipboard operation
AudioSwitcher copied to clipboard

NullReferenceException triggered from other process

Open ygoe opened this issue 5 years ago • 10 comments

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.

ygoe avatar Mar 07 '19 19:03 ygoe

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.

xenolightning avatar Mar 07 '19 19:03 xenolightning

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 avatar Mar 07 '19 19:03 ygoe

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

ygoe avatar Mar 10 '19 14:03 ygoe

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 avatar Oct 18 '19 14:10 roblarky

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.

roblarky avatar Oct 20 '19 14:10 roblarky

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.

pengowray avatar Mar 01 '23 14:03 pengowray

@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

xenolightning avatar Mar 02 '23 02:03 xenolightning

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).

pengowray avatar Mar 03 '23 08:03 pengowray

@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.

Sangheilioz avatar Jan 08 '24 16:01 Sangheilioz

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

xenolightning avatar Jan 08 '24 20:01 xenolightning