Music Player

Although I’m not a DJ, sometimes I like to play selected music from my collection by double clicking the songs, and often I don’t want to wait for a entire song to complete until I play the next one. But in this case Windows® Media Player doesn’t perform cross-fading.

I didn’t want to get third party software to get the behavior I needed, so I created a small Music Player application myself (using WPF and C#) – see code below.

The solution is actually simple, providing a window that displays the list of MP3 songs from a configured folder and sub-folders, and allowing you to play songs one after another by double clicking or hitting the Enter key on the selected item. The cross-fading mechanism is conducted by controlling a set of (invisible) MediaElement and some DispatcherTimer objects. Nothing fancy, but it works just fine. Of course you can add an automatic playlist and many other features to improve it.

MainWindow.xaml:

<Window x:Class="MusicPlayer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MusicPlayer"
        Title="Music Player" Height="350" Width="525">
    <Window.Resources>
        <local:FileNameConverter x:Key="FileNameConverter"/>
    </Window.Resources>
    <Grid>
        <ItemsControl Name="PlayersItemsControl" 
                      ItemsSource="{Binding Players}" Opacity="0">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <MediaElement Source="{Binding Source}"
                                  LoadedBehavior="Manual"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <ListBox Name="AvailableSongsListBox"
                 ItemsSource="{Binding AvailableSongs}"
                 KeyDown="AvailableSongsListBox_KeyDown"
                 MouseDoubleClick="AvailableSongsListBox_MouseDoubleClick">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text=
                     "{Binding Converter={StaticResource FileNameConverter}}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

MainWindow.xaml.cs:

public MainWindow()
{
    InitializeComponent();
    DataContext = Context;
}

private PlayerContext context;
public PlayerContext Context
{
    get 
    {
        if (context == null)
            context = new PlayerContext();
        foreach (Player player in context.Players)
        {
            player.Playing += Player_Playing;
            player.Stopping += Player_Stopping;
        }
        return context;
    }
}

private void AvailableSongsListBox_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
        PlaySelectedFile();
}

private void AvailableSongsListBox_MouseDoubleClick(
    object sender, MouseButtonEventArgs e)
{
    if (e.ChangedButton == MouseButton.Left)
        PlaySelectedFile();
}

private void PlaySelectedFile()
{
    if (AvailableSongsListBox.SelectedItem == null)
        return;
    var song = (string)AvailableSongsListBox.SelectedItem;
    Context.Play(song);
}

private void Player_Playing(object sender, EventArgs e)
{
    var element = PlayersItemsControl.ItemContainerGenerator.
        ContainerFromItem(sender) as FrameworkElement;
    var mediaPlayer = VisualTreeHelper.GetChild(element, 0) as MediaElement;
    DispatcherTimer timer = 
        new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
    timer.Tick += (ts, te) =>
    {
        if (!mediaPlayer.IsLoaded)
            return;
        timer.Stop();
        StartPlaying(mediaPlayer);
    };
    timer.Start();
}

private void StartPlaying(MediaElement mediaPlayer)
{
    mediaPlayer.Volume = 0;
    int count = 30;
    int i = 0;
    DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
    timer.Tick += (ts, te) =>
    {
        i++;
        mediaPlayer.Volume = i < count ? (double)i / count : 1;
        if (i >= count)
            timer.Stop();
    };
    mediaPlayer.Play();
    timer.Start();
}

private void Player_Stopping(object sender, EventArgs e)
{
    DispatcherTimer timer = 
        new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
    timer.Tick += (ts, te) => 
    {
        timer.Stop();
        var element = PlayersItemsControl.ItemContainerGenerator.
            ContainerFromItem(sender) as FrameworkElement;
        var mediaPlayer = VisualTreeHelper.
            GetChild(element, 0) as MediaElement;
        StartFadingOut(mediaPlayer);
    };
    timer.Start();
}

private void StartFadingOut(MediaElement mediaPlayer)
{
    mediaPlayer.Volume = 1;
    int count = 30;
    int i = count;
    DispatcherTimer timer = 
        new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
    timer.Tick += (ts, te) =>
    {
        i--;
        mediaPlayer.Volume = i > 0 ? (double)i / count : 0;
        if (i <= 0)
        {
            mediaPlayer.Stop();
            timer.Stop();
        }
    };
    timer.Start();
}

public class FileNameConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        var fileName = (string)value;
        return fileName.Replace(Path.DirectorySeparatorChar.ToString(), " / ");
    }

    public object ConvertBack(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

PlayerContext.cs:

public class PlayerContext
{
    private Player[] players;
    public Player[] Players
    {
        get
        {
            if (players == null)
            {
                players = new Player[8];
                for (var i = 0; i < players.Length; i++)
                    players[i] = new Player();
            }
            return players;
        }
    }
    private int playerIndex;

    private List<string> availableSongs;
    public List<string> AvailableSongs 
    {
        get 
        {
            if (availableSongs == null)
            {
                availableSongs = new List<string>();
                foreach (string filePath in Directory.GetFiles(
                    @"C:\Music", "*.mp3", SearchOption.AllDirectories))
                    availableSongs.Add(filePath);
            }
            return availableSongs;
        }
    }

    public void Play(string song)
    {
        players[playerIndex++].Stop();
        if (playerIndex >= players.Length)
            playerIndex = 0;
        players[playerIndex].SourcePath = song;
        players[playerIndex].Play();
    }
}

public class Player : INotifyPropertyChanged
{
    private string sourcePath;
    public string SourcePath {
        get { return sourcePath; } 
        set { sourcePath = value; OnPropertyChanged("SourcePath"); 
              OnPropertyChanged("Source"); } }
    public Uri Source { 
        get { return SourcePath != null ? 
              new Uri(SourcePath, UriKind.Absolute) : null; } }

    public event EventHandler Playing;
    public void Play()
    {
        if (Playing != null)
            Playing(this, EventArgs.Empty);
    }

    public event EventHandler Stopping;
    public void Stop()
    {
        if (Stopping != null)
            Stopping(this, EventArgs.Empty);
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion
}
Advertisements

About Sorin Dolha

My passion is software development, but I also like physics.
This entry was posted in .NET, WPF and tagged , , , , . Bookmark the permalink.

One Response to Music Player

  1. Pingback: Music player with auto-crossfading | Code {Sections}

Add a reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s