views:

225

answers:

5

I'm trying to write a drop-in replacement for System.Media.SoundPlayer using the waveOut... API. This API makes a callback to a method in my version of SoundPlayer when the file/stream passed it has completed playback.

If I create a form-scoped instance of my SoundPlayer and play something, everything works fine because the form keeps the object alive, so the delegate is alive to receive the callback.

If I use it like this in, say, a button click event:

SoundPlayer player = new SoundPlayer(@"C:\whatever.wav");
player.Play();

... it works fine 99% of the time, but occasionally (and frequently if the file is long) the SoundPlayer object is garbage-collected before the file completes, so the delegate is no longer there to receive the callback, and I get an ugly error.

I know how to "pin" objects using GCHandle.Alloc, but only when something else can hang onto the handle. Is there any way for an object to pin itself internally, and then un-pin itself after a period of time (or completion of playback)? If I try GCHandle.Alloc (this, GCHandleType.Pinned);, I get a run-time exception "Object contains non-primitive or non-blittable data."

A: 

This might sound too simple, but just make a strong reference to the SoundPlayer object in your own class. That ought to keep GC away as long as the object is alive.

I.e. instead of:

public class YourProgram
{
    void Play()
    {
       SoundPlayer player = new SoundPlayer(@"c:\whatever.wac");
       player.Play();
    }
}

This:

public class YourProgram
{
    private SoundPlayer player;
    void Play()
    {
       player = new SoundPlayer(@"c:\whatever.wac");
       player.Play();
    }
}
Sheeo
+5  A: 

You could just have a static collection of all the "currently playing" sounds, and simply remove the SoundPlayer instance when it gets the "finished playing" notification. Like this:

class SoundPlayer
{
    private static List<SoundPlayer> playing = new List<SoundPlayer>();

    public void Play(...)
    {
        ...
        playing.Add(this);
    }

    // assuming this is your callback when playing has finished
    public void OnPlayingFinished(...)
    {
        ...
        playing.Remove(this);
    }
}

(Obviously locking/multithreading, error checking and so on required)

Dean Harding
I'm actually in the middle of trying this out. You'll get the check if it works!
MusiGenesis
This is not thread safe.
SLaks
@SLaks: As I said, I left that as an exercise for the reader... Also, in your solution, if the 'OnSoundFinished' callback is called from a different thread, then it won't work. It'd be simpler to just have one list and add locks around it.
Dean Harding
@codeka: it is called from a separate thread, so you're right, it doesn't work.
MusiGenesis
Can you add some simple locking code to make it thread-safe *and* functional? :)
MusiGenesis
HashSet is a better choice than List.
Matthew Flaschen
+2  A: 

Your SoundPlayer object should just be stored in a private field of your form class so that it stays referenced long enough. You probably need to dispose it when your form closes.

Fwiw, pinning doesn't work because your class is missing a [StructLayout] attribute. Not that it will work effectively with one, you would have to store the returned GCHandle somewhere so that you can unpin it later. Your form class is the only logical place to store it. Make it simple.

Hans Passant
+1 although I disagree. I'm trying to make it simpler and less of a pain to use SoundPlayer in my code, and also make it able to play more than one sound at a time.
MusiGenesis
You need to keep the internal plumbing that keep the file playing alive. That doesn't have to be part of the SoundPlayer instance. A thread or a "completed" event could be enough, it depends.
Hans Passant
A: 

Are you simply trying to prevent your object from being garbage collected? Couldn't you call GC.KeepAlive( this ) to protect it from the GC?

Thomas
Where inside of the SoundPlayer class would you put this call? If you put in at the end of the constructor, it will keep `this` alive until the end of the constructor, at which point most objects are still alive, anyway. :) `GC.KeepAlive (this)` isn't really functionally different than something like `this.Happiness += 1;` - by referencing `this`, it's just making sure that the object stays alive until that point in the code.
MusiGenesis
You would call it at the end of your Play method. The compiler will see the call and ensure your object is protected from GC until the Play method is completed.
Thomas
I'm assuming that at some point your Play method gets some handle to unmanaged code and that gets GC'd before Play finishes? It would be at the end of your Play method on the unmanaged handle that you would want to call KeepAlive.
Thomas
The Play method (which wraps waveOutWrite) just begins the audio playback and then returns immediately, so it's asynchronous. I need to make sure the object stays alive until the playback completes and the WaveCallback is called. There's no way to do that unless something somewhere keeps a reference to it. In this case, the only thing I can think of is a static internal collection, as in codeka's answer.
MusiGenesis
Given that, I'd agree that Codeka's solution will work better.
Thomas
+1  A: 

The best way to do this is to keep a [ThreadStatic] list of active SoundPlayers in a private static field, and remove each instance from the list when the sound finishes.

For example:

[ThreadStatic]
static List<SoundPlayer> activePlayers;

public void Play() {
    if(activePlayers == null) activePlayers = new List<SoundPlayer>();
    activePlayers.Add(this);
    //Start playing the sound
}
void OnSoundFinished() {
    activePlayers.Remove(this);
}
SLaks
That has a weird effect. I'm using the wave callback (the "done" message) to remove the player from the static list, with code almost identical to yours. Without the `[ThreadStatic]` tag, it works; *with* the tag as above, the `activePlayers.Remove(this);` line fails the first time it's hit, with a NullReferenceException that claims activePlayers is null. This is definitely *after* the initial call to Play which instantiates activePlayers.
MusiGenesis
This is probably because the callback is being called on a different thread to one that started playing the sound. Just use a non-ThreadStatic instance and put locks around it to make it thread-safe.
Dean Harding
Is there a different way to make this thread-safe? OnSoundFinished(...) is called from a different thread, which explains the null reference exception.
MusiGenesis
@MusiGenesis: You could use one of the thread safe collections from .Net 4.
SLaks