13 January 2013

Playing sounds on Windows Phone using the MVVMLight Messenger

In my never-ending quest to preach the gospel of MVVM in general and MVVMLight in particular as the way to make a structured Windows Phone application I show a little part of my my newest Windows Phone app, “Pull the Rope”. It’s a basically a rope pulling contest played on two phones. I think it’s quite fun to play but I am pretty sure it’s even more hilarious to watch other people play it, swiping like maniacs on their phones.

The first version did not even have sounds – I decided to go the “ship early ship often” route this time – so some days ago I submitted a version that does some supportive sound. Of course my game is MVVMLight based and for adding the game sound I pulled a tried-and-tested (at least, by me) out of the hat – the Messenger-Behavior combo.Using the Messenger requires of course a message, so I started off with that:

namespace Wp7nl.Audio
{
  public class PlaySoundEffectMessage
  {
    public PlaySoundEffectMessage(string soundName, bool start = true)
    {
      SoundName = soundName;
      Start = start;
    }

    public string SoundName { get; private set; }

    public bool Start { get; private set; }
  }
}

So this message has only two options – an identifier for the sound that must be started, and a boolean that indicates whether the sound should be started (default) or stopped (this for sounds that are played in a loop).

Then the behavior itself. As usual, I start off with a couple of Dependency Properties, to support data binding:

  • SoundFileLocation (string) – the location of the sound file to play
  • SoundName (string) – the sound identifier; if PlaySoundEffectMessage.SoundName has the same value a this property, the behavior will take action.
  • Repeat (bool) – indicates if the sound should be played in a loop or not.

I hope you will forgive me for not including the Dependency Properties’ code in the this article as it is pretty much run of the mill and takes a lot of space.

The core of the behavior itself is actually pretty simple. It’s meant to be used in conjunction with a MediaElement. First, the setup. I created this as a SafeBehavior to make setup and teardown a little less complex:

using System;
using System.Windows;
using System.Windows.Controls;
using GalaSoft.MvvmLight.Messaging;
using Wp7nl.Behaviors;

namespace Wp7nl.Audio
{
  public class PlaySoundEffectBehavior : SafeBehavior<MediaElement>
  {
    protected override void OnSetup()
    {
      Messenger.Default.Register<PlaySoundEffectMessage>(
        this, DoPlaySoundFile);
      AssociatedObject.IsHitTestVisible = false;
      AssociatedObject.AutoPlay = false;
      var soundUri = new Uri(SoundFileLocation, UriKind.Relative);
      AssociatedObject.Source = soundUri;
      AssociatedObject.Position = TimeSpan.FromSeconds(0);
      SetRepeat(Repeat);
    }
  }
}

So basically this behavior subscribes to the message type we just defined. Then it goes on initializing the MediaElement – disabling hit test and autoplay, actually setting the sound file URI, initializing it to the beginning and initializing repeat (or not).

The method DoPlaySoundFile, which kinda does all the work, isn’t quite rocket science either:

private void DoPlaySoundFile(PlaySoundEffectMessage message)
{
  if (SoundName == message.SoundName)
  {
    if (message.Start)
    {
      AssociatedObject.Position = TimeSpan.FromSeconds(0);
      AssociatedObject.Play();
    }
    else
    {
      AssociatedObject.Stop();
    }
  }
}

If a message is incepted with the same sound name as has been set to the behavior in the XAML, then either start or stop the sound.

The rest of the behavior is basically some odds and ends:

private void SetRepeat(bool repeat)
{
  if (AssociatedObject != null)
  {
    if (repeat)
    {
      AssociatedObject.MediaEnded += AssociatedObjectMediaEnded;
    }
    else
    {
      AssociatedObject.MediaEnded -= AssociatedObjectMediaEnded;
    }
  }
}

private void AssociatedObjectMediaEnded(object sender, RoutedEventArgs e)
{
  AssociatedObject.Position = TimeSpan.FromSeconds(0);
  AssociatedObject.Play();
}

protected override void OnCleanup()
{
  Messenger.Default.Unregister(this);
  AssociatedObject.MediaEnded -= AssociatedObjectMediaEnded;
}

The first method, SetRepeat, enables or disables repeat. As a MediaElement does not support the endless SoundMvvmloop by itself, repeat as such is implemented by subscribing the method AssociatedObjectMediaEnded to the MediaEnded event of the MediaElement – that does nothing more than kicking off the sound again. If repeat has to be turned off, the AssociatedObjectMediaEnded is unsubscribed again and the sound automatically ends.

Finally, the last method is called when the behavior is deactivated. It removes the messenger subscription and a possible repeat event subscription.

So how would you go about and use such a behavior? To demonstrate it’s working, I have created a small sample solution with the very exiting *cough* user interface showed to the right. What this app does is, as you click on the go button, is fire off the PlaySoundCommand command in the following, admittedly somewhat contrived view model:

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
using Wp7nl.Audio;

namespace SoundMvvm.Viewmodels
{
  public class SoundViewModel : ViewModelBase
  {
    public ICommand PlaySoundCommand
    {
      get
      {
        return new RelayCommand(
          () =>
            {
              var t = new DispatcherTimer {Interval =  
                      TimeSpan.FromSeconds(4)};
              t.Tick += TimerTick;
              t.Start();
            });
      }
    }

    private void TimerTick(object sender, EventArgs e)
    {
      tickNumber++;
      switch (tickNumber)
      {
        case 1:
          Messenger.Default.Send(new PlaySoundEffectMessage("Sad trombone"));
          break;
        case 2:
          Messenger.Default.Send(new PlaySoundEffectMessage("Ping"));
          break;
        case 3:
          Deployment.Current.Dispatcher.BeginInvoke(() => 
            Messenger.Default.Send(new PlaySoundEffectMessage("Ping", false)));
          var t = sender as DispatcherTimer;
          t.Stop();
          t.Tick -= TimerTick;
          break;
      }
    }

    private int tickNumber;
  }
}

This initializes and starts a DispatcherTimer that will fire every four seconds. So the first four seconds after you click the button absolutely nothing will happen – I wanted to simulate the situation in which an event in the view model, not necessarily directly kicks off the sound. At the first tick – after four seconds – the view model fires a message making the behavior start the “Sad trombone” sound, which runs about four seconds. Then it fires off the “Ping” message, which causes the “Ping” sound to be started and repeated by the behavior. After another four seconds (good for about three ‘pings’) it’s killed again by the second “Ping” message. And then this little app has done all it could. Cue sad trombone indeed ;-)

As to the XAML to make this all work, I’ve outlined in red (and underline for the color blind readers) the interesting parts:

<phone:PhoneApplicationPage
    x:Class="SoundMvvm.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:audio="clr-namespace:Wp7nl.Audio;assembly=Wp7nl.MvvmLight"
    xmlns:viewmodels="clr-namespace:SoundMvvm.Viewmodels"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True" >

  <phone:PhoneApplicationPage.Resources>
    <viewmodels:SoundViewModel x:Key="SoundViewModel" />
  </phone:PhoneApplicationPage.Resources>
  <Grid x:Name="LayoutRoot" Background="Transparent" 
DataContext="{StaticResource SoundViewModel}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="DEMO MVVM SOUNDS"
Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/> <TextBlock Text="play sounds" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/> </StackPanel> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Button Content="go!" Margin="0,31,0,0" VerticalAlignment="Top" Width="128" Height="80" Command="{Binding PlaySoundCommand}"/> </Grid> <MediaElement> <i:Interaction.Behaviors> <audio:PlaySoundEffectBehavior SoundName="Sad trombone" SoundFileLocation="/Sound/Sad_Trombone-Joe_Lamb-665429450.mp3"/> </i:Interaction.Behaviors> </MediaElement> <MediaElement> <i:Interaction.Behaviors> <audio:PlaySoundEffectBehavior Repeat="True" SoundName="Ping" SoundFileLocation="/Sound/Elevator-Ding-SoundBible.com-685385892.mp3"/> </i:Interaction.Behaviors> </MediaElement> </Grid> </phone:PhoneApplicationPage>

On top we have the DataContext set to our viewmodel, which, by using it this way, is automatically instantiated. The “go!” button is bound to the PlaySoundCommand, and then you see near the bottom the two MediaElement objects which each a PlaySoundEffectBehavior. The first one plays the “Sad trombone" once as the message is received, the second repeatedly (Repeat=”True”) the “ping” sound until it receives a message with “Start” the to “false” indicating it should shut up.

In the sample solution you will find two projects: SoundMvvm (the app itself) and Wpnl.Contrib, which both the behavior and the message class. As you probably understand from this setup, both classes will be soon in the wp7nl CodePlex library.For the record, this is a Windows Phone 8 app, but I think this should work on Windows Phone 7 as well, although on 7 I would still go for the XNA SoundEffect class. The technique used for this behavior comes from my Catch’em Birds for Windows 8 app by the way, where you don’t have XNA at all.

No comments: