Simple player

In this tutorial we are going to create a simple application with MIDI playback function.

First of all we have to create a new WPF project and add references to Manufaktura.Controls and Manufaktura.Controls.Desktop and Manufaktura.Model libraries. First library contains all the base classes needed to render and playback scores. The second library contains the MIDI player implementation and the third one is required by the first one.

We will add a simple button to MainWindow.xaml:

<Window x:Class="Manufaktura.Tutorials.Midi.MainWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Manufaktura.Tutorials.Midi"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Button Click="Button_Click" Content="Play" />
    </Grid>
</Window>

 

 No we have to handle the button's click event in code behind:

ScorePlayer player;        
private void Button_Click(object sender, RoutedEventArgs e)

{
            var dialog = new OpenFileDialog();

            if (dialog.ShowDialog(this) ?? false)
            {

                var content = File.ReadAllText(dialog.FileName);
              var score = content.ToScore();
                player = new MidiTaskScorePlayer(score);
                player.Play();

            }

}

 Let's run the application. After pressing the Play button we will be prompted to choose a MusicXml file which will then be played. 

More complex player

Now we are going to make some more complex player. First of all we are going to add reference to another library Manufaktura.Controls.WPF which contains the WPF NoteViewer control. We will now add NoteViewer control to main window and create some more complex layout:

<Window x:Class="Manufaktura.Tutorials.Midi.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       xmlns:local="clr-namespace:Manufaktura.Tutorials.Midi"
        xmlns:Manufaktura="clr-namespace:Manufaktura.Controls.WPF;assembly=Manufaktura.Controls.WPF"
        mc:Ignorable="d"
      Title="MainWindow" Height="350" Width="525">

    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>

        <ScrollViewer>
            <Manufaktura:NoteViewer />
        </ScrollViewer>

        <StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="Open" Width="100" /> <Button Content="Play" Width="100" /> <Button Content="Stop" Width="100" />
        </StackPanel>

    </Grid>
</Window>

 Now we are going to create a viewmodel which will store our score model and notify the view about all changes:

public class MainViewModel : ViewModel
{
        private ScorePlayer player;
      public ScorePlayer Player => player;

        private Score score;
        public Score Score
        {
          get
            {
                return score;
            }
            set
            {
                score = value;
if (player != null) ((IDisposable)player).Dispose(); //This is needed in Midi player. Otherwise it can throw a "Device not ready" exception.
                player = new MidiTaskScorePlayer(score);
                OnPropertyChanged();
                OnPropertyChanged(() => Player);
            }
        }
}

 Now let's implement some logic:

public abstract class PlayerCommand : ICommand
    {

        public event EventHandler CanExecuteChanged;


        protected MainViewModel viewModel;

        protected PlayerCommand(MainViewModel viewModel)
        {
            this.viewModel = viewModel;
       }

  public void FireCanExecuteChanged() { CanExecuteChanged?.Invoke(this, new EventArgs()); }

        public abstract bool CanExecute(object parameter);

        public abstract void Execute(object parameter);

    }

Playback:

 public class PlayCommand : PlayerCommand
    {

        public PlayCommand(MainViewModel viewModel) : base(viewModel)
        {
        }

 

        public override bool CanExecute(object parameter)
        {
            return viewModel.Player != null;
        }

        public override void Execute(object parameter)
        {
          viewModel.Player?.Play();
        }

    }

public class StopCommand : PlayerCommand { public StopCommand(MainViewModel viewModel) : base(viewModel) { } public override bool CanExecute(object parameter) { return viewModel.Player != null; } public override void Execute(object parameter) { viewModel.Player?.Stop(); } }

And file loading:

 public class OpenCommand : PlayerCommand
    {
        public OpenCommand(MainViewModel viewModel) : base(viewModel)
        {
        }

         public override bool CanExecute(object parameter)
        {
           return true;
        }
 
        public override void Execute(object parameter)
        {

            var dialog = new OpenFileDialog();
            if (dialog.ShowDialog() ?? false)
           {

                var content = File.ReadAllText(dialog.FileName);
                viewModel.Score = content.ToScore();
            }
        }
    }

 

We will add these commands to our viewmodel:

    public class MainViewModel : ViewModel
    {

        private ScorePlayer player;

        public OpenCommand OpenCommand { get; }
        public PlayCommand PlayCommand { get; }
public StopCommand StopCommand { get; }

        public MainViewModel()
        {
            OpenCommand = new OpenCommand(this);
            PlayCommand = new PlayCommand(this);
StopCommand = new StopCommand(this);
        }

 

        public ScorePlayer Player => player;

        private Score score;

        public Score Score

        {

            get
            {
                return score;
            }

            set
            {
score = value; if (player != null) ((IDisposable)player).Dispose(); player = new MidiTaskScorePlayer(score); OnPropertyChanged(); OnPropertyChanged(() => Player); PlayCommand?.FireCanExecuteChanged(); StopCommand?.FireCanExecuteChanged();
            }
        }
    }

 

 No let's completely clear the code behind class for MainWindow and just set the data context:

 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel();
        }
    }

The last thing is to bind NoteViewer control and buttons to viewmodel:

        <ScrollViewer>
            <Manufaktura:NoteViewer ScoreSource="{Binding Score}" PlaybackCursorPosition="{Binding Player.CurrentPosition}" RenderingMode="AllPages"/>
        </ScrollViewer>

        <StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="Open" Width="100" Command="{Binding OpenCommand}" /> <Button Content="Play" Width="100" Command="{Binding PlayCommand}" /> <Button Content="Stop" Width="100" Command="{Binding StopCommand}" />
        </StackPanel>



After opening a file in our application, it will be displayed in NoteViewer control. This behavior is achieved by binding viewmodel's Score property to ScoreSource property of NoteViewer. The second property (optional) is the position of playback cursor which is displayed as vertical purple line on the control. It will be updated each time a note is played. If PlaybackCursorProperty is not bound, the cursor will not be displayed.

Creating your own ScorePlayer

Manufaktura.Controls.Desktop library contains an implementation of playback using MIDI. You can create your own implementations using other media. The simplest way to achieve that is to derive from TaskScorePlayer class, base class of MidiTaskScorePlayer. There is only one abstract method PlayElement that you have to implement. This method is called every time a single element (for example note) has to be played. For example if there are four quarter notes in tempo of 120 quarter notes per minute, the PlayElement method will be called two times per second. If there is more than one element to play at a time (for example a chord) PlayElement method will be called a few times one by one. Implementation has to be asynchronous to make sure that these elements are played simultaneously. 

This is a test player which writes elements to console instead of playing: 

    public class TestScorePlayer : TaskScorePlayer
    {
        public TestScorePlayer(Score score) : base(score)
        {
        }

        public override void PlayElement(MusicalSymbol element)
        {
           Console.WriteLine(element);
        }
   }

 

If TaskScorePlayer doesn't suit your needs you can create a player that derives from ScorePlayer directly and implement your own Play, Pause, Stop and PlayElement methods. To iterate the playable elements in score use EnumerateTimeline() method. It will fetch objects in playback order. The returned objects are of type TimelineElement<IHasDuration>. This class contains two fields:

  • What - an object that is to be played,
  • When - time when object is to be played. 

To better explain this mechanism, this is a TaskScorePlayer implementation:

public abstract class TaskScorePlayer : ScorePlayer
{
private IEnumerator<TimelineElement<IHasDuration>> timelineInterator;

public TaskScorePlayer(Score score) : base(score)
{
timelineInterator = EnumerateTimeline().GetEnumerator();
}

public override MusicalSymbol CurrentElement
{
get
{
return EnumerateTimeline().FirstOrDefault(t => t.When == ElapsedTime)?.What as MusicalSymbol;
}

protected set
{
throw new Exception("CurrentElement method is readonly on this class.");
}
}

public override TimeSpan ElapsedTime
{
get
{
return base.ElapsedTime;
}

set
{
base.ElapsedTime = value;
OnPropertyChanged(() => CurrentElement);
OnPropertyChanged(() => CurrentPosition);
}
}

public override void Pause()
{
State = PlaybackState.Paused;
}

public override async void Play()
{
State = PlaybackState.Playing;
await Task.Factory.StartNew(PlayInternal);
}

public override void Stop()
{
State = PlaybackState.Idle;
timelineInterator = EnumerateTimeline().GetEnumerator();
}

protected virtual void PlayQueue(Queue<TimelineElement<IHasDuration>> simultaneousElements)
{
lock (simultaneousElements)
{
while (simultaneousElements.Any())
{
var element = simultaneousElements.Dequeue();
if (ElapsedTime != element.When) ElapsedTime = element.When;
var note = element.What as Note;
if (note == null) continue;

PlayElement(note);
}
}
}

private async void PlayInternal()
{
var simultaneousElements = new Queue<TimelineElement<IHasDuration>>();

TimelineElement<IHasDuration> previousElement = null;
TimeSpan lastAwaitedDuration = TimeSpan.Zero;

while (timelineInterator.MoveNext())
{
var timelineElement = timelineInterator.Current;
if (State != PlaybackState.Playing) break;

if (previousElement != null && timelineElement.When > previousElement.When)
{
await Task.Delay(lastAwaitedDuration == TimeSpan.Zero ? previousElement.When : previousElement.When - lastAwaitedDuration);
PlayQueue(simultaneousElements);
lastAwaitedDuration = previousElement.When;
}

simultaneousElements.Enqueue(timelineElement);
previousElement = timelineElement;
}

if (simultaneousElements.Any())
{
await Task.Delay(lastAwaitedDuration == TimeSpan.Zero ? previousElement.When : previousElement.When - lastAwaitedDuration);
PlayQueue(simultaneousElements);
}
}
}



I hope it helped :)

Sample application

Download