1
votes

Coding an app using WPF framework in C# (I'm really really new to it, don't be too hard on my coding style). I want some "goodbye" audio to play (using NAudio 1.8.4 reading a stream from a MPQ file) when I exit and not to be interrupted even if the main window disappeared ; for this reason I use a foreground thread in a class called PlayerThread.

I play sounds using the PlayerThread constructor

 foregroundChannel = new PlayerThread(soundStream, null, null);

but I don't keep track of it later, I just rely on the PlayerThread to end "naturally" after playing.

Here is the PlayerThread constructor :

public PlayerThread(MpqFileStream stream, Action<PlayerThread> over, Dispatcher callerDispatcher)
    {
        noCallback = false;
        dispatcher = callerDispatcher;
        fadeOut = false;
        soundStream = stream;
        ThreadStart threadStart = RunThread;
        if (callerDispatcher != null)
        {
            callback = over;
            threadStart += () =>
            {
                dispatcher.Invoke(() =>
                {
                    Cleanup();
                });
            };
        }
        this.m_playerThread = new Thread(RunThread)
        {
            Name = "Audio Player Thread",
            IsBackground = false
        };
        this.m_playerThread.SetApartmentState(ApartmentState.STA);
        this.m_playerThread.Start();
    }

and here is the work it does :

private void RunThread()
    {
        internalPlayer = new WaveOut();
        Mp3FileReader mp3FileReader = new Mp3FileReader(soundStream);
        inputStream = new WaveChannel32(mp3FileReader);
        internalPlayer.Init(inputStream);
        internalPlayer.Play();
        while (internalPlayer.PlaybackState == PlaybackState.Playing && !fadeOut)
        {
            System.Threading.Thread.Sleep(100);
        }
        if (fadeOut)
        {
            while (((float)0F).CompareTo(inputStream.Volume) < 0)
            {
                System.Threading.Thread.Sleep(150);
                inputStream.Volume -= 0.1F;
            }
        }
        internalPlayer.Dispose();
        inputStream.Dispose();
        mp3FileReader.Dispose();
        soundStream.Dispose(); // Yeah I'm so desperate for a solution I'm just Disposing everything even though I don't think it helps
    }

When clicking on exit, these lines are executed :

soundEngine.FadeOutBackground();
soundEngine.PlayVoice(SoundNamesConstants.BYE);
soundEngine.WaveGoodbye();
Close();
soundEngine.WaitForTheCrewBeforeWeighingAnchor();

Eventually, here's is the code inside Cleanup() :

if (!noCallback)
            callback.Invoke(this);

The callback is pretty irrelevant here, it's something I use to switch between musics with a fade out effect. Plus it doens't occur in this instance as I just set noCallback to false.

If it's any use, here's the resource usage chart. The red line is when I press exit.

resource usage chart


UPDATE Added Thread.Join() synchronisation as suggest by @sacha :

public void WaitForTheCrewBeforeWeighingAnchor() // yes, every word matters
    {
        backgroundChannel.Join();

        foreach (PlayerThread pt in foregroundChannels)
            pt.Join();
    }

It has no problem Joining with the backgroundChannel thread but never Joins with the only thread in foregroundChannels. Inverting line 3 with lines 5-6 has the same result, systematically failing to pt.Join();. This PlayerThread is the one created when clicking on exit.


UPDATE 2 This is not a threading issue. This is only happening because the condition internalPlayer.PlaybackState == PlaybackState.Playing isn't always met (checked using breakpoints), so we just stay in the while loop. Currently looking for a better way to check playing is over.

2
Have you tried putting a breakpoint in RunThread to see where it gets stuck?Kevin Gosse
Yep, the background music thread executes till the end. The "goodbye" sound doesn't. No idea why I just keep tinkering with values and breakpoints and nothing changes. :Cobalt Scales
Programmers are drawn to Dispatcher.Invoke() like moth to a flame. Always, always favor BeginInvoke(). Invoke is likely to cause deadlock, all it takes is for the UI thread to stop dispatching or be occupied with something else. Both occur here, it stops dispatching when the main window is closed. And is occupied by something else when you call Thread.Join(). nocallback = false; is not actually implemented, should be = true anyway. Use Debug > Windows > Threads to diagnose the deadlock.Hans Passant
@HansPassant Thank you for the valuable advice, I changed it to BeginInvoke with a proper delegate. As for nocallback = false; it is implemented through soundEngine.WaveGoodbye(); which sets nocallback to false. It's useful because the callback actually checks if another background music is in the queue (when using backgroundChannel) and we don't want that when exiting. Alas problem persists as all this doesn't fix the while exit problem which happens because the PlaybackState property of WaveOut is not always changed when the sound stops playing (I checked this using breakpoints)...Cobalt Scales

2 Answers

1
votes

WaveChannel32 constructor creates a zero filled buffer which can be useful for scenarios where you are feeding into a mixer. In this case, it just caused my stream to "never" end.

Adding :

inputStream.PadWithZeroes = false;

Solved my problem entirely. Joining isn't required in this situation.

For more information :

The Never-Ending Stream Problem

The way that each implementor of IWavePlayer determines whether it should automatically stop playback is when the Read method on the source IWaveProvider returns 0 (In fact Read should always return the count parameter unless the end of the stream has been reached).

However, there are some WaveStreams / IWaveProviders in NAudio that never stop returning audio. This isn’t a bug – it is quite normal behaviour for some scenarios. Perhaps BufferedWaveProvider is the best example – it will return a zero-filled buffer if there is not enough queued data for playback. This is useful for streaming from the network where data might not be arriving fast enough but you don’t want playback to stop. Similarly WaveChannel32 has a property called PadWithZeroes allowing you to turn this behaviour on or off, which can be useful for scenarios where you are feeding into a mixer.

From : http://mark-dot-net.blogspot.be/2011/05/naudio-and-playbackstopped-problem.html

0
votes

Sounds like you need to synchronize your threads where main thread should wait for other thread to finish before exit. Have a look at Thread.Join : https://msdn.microsoft.com/en-us/library/95hbf2ta(v=vs.110).aspx