2

I am using System.Windows.Media.MediaPlayer for playing .wma audio file of 5 seconds length .

I was not able to find any direct option for repeatedly playing this media file so I have implemented looping logic as follows. But it seems to be not working.

For the following code, MediaEnded event is not getting fired even after playback is ended. What am I doing wrong here?

public void PlayAudio(string audioFilePath, TimeSpan duration)
{
    var thread = new Thread(() => { PlayAudioThreadProc(audioFilePath, duration); });
    thread.Start();
}
private void PlayAudioThreadProc(string audioFilePath, TimeSpan duration)
{
    MediaPlayer mediaPlayer = CreateMediaPlayer(audioFilePath);
    mediaPlayer.Play();

    _stopSignal.WaitOne(duration);

    Stop(mediaPlayer);
}
private static MediaPlayer CreateMediaPlayer(string audioFilePath)
{
    var mediaPlayer = new MediaPlayer();
    mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;     //This event is not getting fired.
    mediaPlayer.Open(new Uri(audioFilePath));
    return mediaPlayer;
}
private static void MediaPlayer_MediaEnded(object sender, EventArgs e)
{
    //This part of code is supposed to start the media again after it is ended playing.
    var player = (MediaPlayer)sender;
    player.Position = TimeSpan.Zero;
    player.Play();
}
private static void Stop(MediaPlayer mediaPlayer)
{
    mediaPlayer.Stop();
    mediaPlayer.MediaEnded -= MediaPlayer_MediaEnded;
    mediaPlayer.Close();
}

Looping logic in above code is not working.

If above approach is not possible, please recommend me another audio player which supports Volume adjustments and Repeat media option. (I tried System.Media.SoundPlayer but it does not support Volume adjustments and .wma files are also not supported in it.)

Kamalesh Wankhede
  • 1,475
  • 16
  • 37
  • Your "duration" i assume is the duration of the audio file. Why not use mediaPlayer.NaturalDuration. because it's normally a bit longer, it looks like you're calling Stop when the duration timeout occurs which unhooks the event and shuts everything down (Possibly before the media player has finished?) – Skintkingle Mar 09 '17 at 11:48
  • No. Actually, duration is the total time for which I want to repeatedly play that media file. For eg. 'audio file duration' is 5 sec and 'duration' parameter contains 10 seconds, then that audio file is supposed to be played twice. – Kamalesh Wankhede Mar 09 '17 at 12:01

3 Answers3

4

First, you seem to be using the MediaPlayer class incorrectly. It inherits DispatcherObject and also is not blocking, so it should really be used on the UI thread.

Now on the main subject.

I was not able to find any direct option for repeatedly playing this media file

Well, actually it supports everything you need except the total play duration time. But you are right - it's not so direct (as most of the WPF stuff) - the repeating is achieved by using MediaTimeline through RepeatBehavior property. You can find sample usage in How to: Play Media using a VideoDrawing. So the basic playing code with repeating is like this:

var timeLine = new MediaTimeline(new Uri(audioFilePath));
timeLine.RepeatBehavior = RepeatBehavior.Forever;
var mediaPlayer = new MediaPlayer();
mediaPlayer.Clock = timeLine.CreateClock();
mediaPlayer.Clock.Controller.Begin();

You create MediaTimeline object, set properties (use RepeatBehavior.Forever to get indefinite repeating, but you can also use the constructor and specify concrete count), then create MediaClock from it and assign it to the MediaPlayer.Clock property. Make sure to read the documentation, because in this mode you should not use Position property and Play, Pause and Stop methods of the MediaPlayer class, but the Clock.Controller methods.

The MediaTimeline also has a property Duration, but it allows you (along with the BeginTime property) to select a portion of the audio file to be played, hence cannot be used to set up the total play duration. So the play time duration problem should be solved in a separate way.

The easiest way I see to support what you need, along with stop function, is to use async method:

public async Task PlayAudioAsync(string audioFilePath, TimeSpan duration, CancellationToken cancellationToken)
{
    var timeLine = new MediaTimeline(new Uri(audioFilePath));
    timeLine.RepeatBehavior = RepeatBehavior.Forever;
    var mediaPlayer = new MediaPlayer();
    mediaPlayer.Clock = timeLine.CreateClock();
    mediaPlayer.Clock.Controller.Begin();
    try
    {
        await Task.Delay(duration, cancellationToken);
    }
    finally
    {
        mediaPlayer.Clock.Controller.Stop();
    }
}

Task.Delay will give you the desired play duration, and CancellationToken - the stop functionality.

Jus make sure to call it from UI thread. Here is a sample usage:

XAML:

<Canvas>
    <Button x:Name="playButton" Content="Play" Width="75" RenderTransformOrigin="1.2,5.24" Canvas.Left="98" Canvas.Top="135" Click="playButton_Click"/>
    <Button x:Name="stopButton" Content="Stop" IsEnabled="False" Width="75" Canvas.Left="208" Canvas.Top="135" Click="stopButton_Click"/>
</Canvas>

Code behind:

private CancellationTokenSource ctsPlay;

private async void playButton_Click(object sender, RoutedEventArgs e)
{
    string audioFile = ...;
    TimeSpan duration = ...;

    ctsPlay = new CancellationTokenSource();
    playButton.IsEnabled = false;
    stopButton.IsEnabled = true;
    try
    {
        await PlayAudioAsync(audioFile, duration, ctsPlay.Token);
    }
    catch
    {
    }
    ctsPlay.Dispose();
    ctsPlay = null;
    stopButton.IsEnabled = false;
    playButton.IsEnabled = true;
}

private void stopButton_Click(object sender, RoutedEventArgs e)
{
    ctsPlay.Cancel();
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
1

MediaPlayers "Play" is not thread locking. the thread ends execution as soon as the sound starts playing. I've setup a local class testing this out and I get the event to fire (On a background thread) like this (I've changed it to be OOP, not a statically used class, you have to call Stop from somewhere else):

public class MediaStuff
{
    private bool _closing = false;
    public void PlayAudio(string audioFilePath)
    {
        var thread = new Thread(() => { PlayAudioThreadProc(audioFilePath); });
        thread.Start();
    }
    private void PlayAudioThreadProc(string audioFilePath)
    {
        MediaPlayer mediaPlayer = CreateMediaPlayer(audioFilePath);
        mediaPlayer.Play();
        while (!_closing)
        {
            System.Threading.Thread.Sleep(10);
            Dispatcher.Run();
        }
        mediaPlayer.Stop();
        mediaPlayer.MediaEnded -= MediaPlayer_MediaEnded;
        mediaPlayer.Close();
    }
    private MediaPlayer CreateMediaPlayer(string audioFilePath)
    {
        var mediaPlayer = new MediaPlayer();
        mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;     //This event is not getting fired.
        mediaPlayer.Open(new Uri(Path.GetFullPath(audioFilePath)));
        return mediaPlayer;
    }

    private void MediaPlayer_MediaEnded(object sender, EventArgs e)
    {
        //This part of code is supposed to start the media again after it is ended playing.
        var player = (MediaPlayer)sender;
        player.Position = TimeSpan.Zero;
        player.Play();
    }
    public void Stop()
    {
        _closing = true;
    }
}

I too had your problem when testing your code. After changing and testing my code, the event now fires, and the sound loops.

Skintkingle
  • 1,579
  • 3
  • 16
  • 28
  • I removed the "duration" parameter and usage because I incorrectly assumed its usage before you replied to my comment, I'm sure you can feed it back into what I've presented you :) – Skintkingle Mar 09 '17 at 12:06
  • Your solution works. It works only if we use `Application.DoEvents();` in a loop like you suggested. According to [this](http://stackoverflow.com/a/1115410/6170142) post, we should avoid using `Application.DoEvents()`. Can you please explain why it is needed and is there any alternative approach for this to work. Also, thanks for pointing me in the right direction. – Kamalesh Wankhede Mar 09 '17 at 12:36
  • I've just had a brain fart. I have no idea why I suggested Application.DoEvents.... I blame a lazy day!... Updating answer. You should use the thread Dispatcher... – Skintkingle Mar 09 '17 at 12:43
  • I still don't get it. Why do we need either `Application.DoEvents` or `Dispatcher.Run`? Why cant we just let the worker thread sleep and still get `MediaEnded` event? – Kamalesh Wankhede Mar 09 '17 at 12:58
  • Because the sleeping is thread locking. This means events cant be fired... the thread is sleeping. And then it immediately sleeps again. doing Dispatcher.Run puts the execution of the current location to the back of the stack of things to deal with. so events being fired can be processed and then execution will continue. – Skintkingle Mar 09 '17 at 14:19
  • 1
    Do you need this on a seperate thread. As "Play" is not thread locking you should be able to do all of this on your UI thread (Do you have a UI that is already running a message loop or are you in a console?) It's also worth noting the dispatcher here is NOT your UI dispatcher, because it's being called from a new thread. It will only process events for your current thread. – Skintkingle Mar 09 '17 at 14:42
1

Alternatively, you can use mciSendString(). Tried to make an example and it succeed, try this;

winmm.dll import,

[DllImport("winmm.dll")]
private static extern long mciSendString( string command, string returnValue,
int returnLength, IntPtr winHandle);

Need to catch operations done by mciSendString(), so we need WndProc;

private const int MM_MCINOTIFY = 0x03b9; // notify mci completed operation
private const int MCI_NOTIFY_SUCCESS = 0x01; // mci successfully executed command

protected override void WndProc(ref Message m)
{
     if (m.Msg == MM_MCINOTIFY)
     {
         switch (m.WParam.ToInt32())
         {
             case MCI_NOTIFY_SUCCESS: // if successfull
                 if (IsLoop) // check if we gave parameter true
                     PlayMediaFile(IsLoop); // so it should run forever, call again function
                 break;
             default:
                 break;
         }
     }
     base.WndProc(ref m);
 }

Method that executes, media file by using mciSendString() that we imported at the beginning

private bool IsLoop = false; // need this to check inside WndProc
private void PlayMediaFile(bool RepeatForever)
{
     IsLoop = RepeatForever; 
     mciSendString("close voice1", null, 0, this.Handle); // first close, after first run, the previous opened file should be terminated
     mciSendString("stop voice1", null, 0, this.Handle); // close file
     string playCommand = "open  " + "yourFilePath" + " type waveaudio alias voice1"; // open command string
     mciSendString(playCommand, null, 0, this.Handle); // open file
     mciSendString("play voice1 notify", null, 0, this.Handle); // play file
}

Then call the method by giving parameter anywhere.

Hope helps,

Berkay Yaylacı
  • 4,383
  • 2
  • 20
  • 37