This article is compatible with the latest version of Silverlight.
Introduction
In this article I will present the Silverlight application I was busy with recently - the Silverlight Metronome. It's a classical metronome (a measurement tool for tempo, mostly used by musicians) that is based on a pendulum with a moveable mass, which modifies its period. The project was very challenging as it needed some simple physics and a beautiful design to be implemented. In my opinion I have accomplished it or at least I have done it according to the physics; the design has always been a very subjective topic. Anyway, you can see the live demo here and download the source code here, but I'm sure that you would like from us to make a fast dissection of the application and see how it works.
The pendulum
I am not using images in this application, which means that our main tool will be the templating of the Silverlight controls. First of all we need a pendulum that will be used for the main functionality of the metronome. We need a control that will give us the possibility to control and change its value - obviously the Slider control suits our needs perfectly.
Besides changing the template of the Slider I need to set some additional properties:
- Orientation to Vertical
- IsDirectionReversed to true (this is because by default the vertical slider has it's direction set from the bottom to the top and for those who don't know the metronome scale – it has its smallest value at the top and its greatest at the bottom)
- Minimum and Maximum to 40 and 208 (by some kind of convention these are the minimal and the maximal tempos for metronomes)
- Small- and LargeChange - it's a good practice these two to be set according to the values that the slider displays. For example, the tempos will be integers with a difference between them 1, so obviously our SmallChange proeprty is set to 1. In order to be more comfortable and to move faster through the tempos I set the LargeChange to 10.
Note: I will not discuss the template here, because it's a really big amount of code. You can see it in the source code.
I also implement the INotifyPropertyChanged interface which raises a PropertyChanged event when the value of the slider is changed:
private void PendulumSlider_ValueChanged( object sender, RoutedPropertyChangedEventArgs<double> e )
{
if ( this.PropertyChanged != null )
{
this.PropertyChanged( this, new PropertyChangedEventArgs( "Tempo" ) );
}
}
and add a public method called SetAngle that later will be used to rotate the pendulum using a RotateTransform:
public void SetAngle( double angle )
{
this.PendulumRotatateTransofrm.Angle = angle;
}
Here is how the Slider looks like after we have finished with our modifications:
The metronome
The next step is to create a body for our metronome that hosts the pendulum. This control also implements the main logic around the movement of the pendulum. Let's start with creating the body. To accomplish this task I use Visual Studio in combination with Expression Blend:
- In the Visual Studio I create the outlines of the body using Polyline controls and setting their points manually.
- In the Expression Blend I choose the colors and fill the outlines using gradients and solid color brushes.
Here is the final result:
Now let's take a look at the functionality around the pendulum. If you have already read the article of Gozzo Smith about the Custom Animations in Silverlight you should be familiar with the physics around the mathematical pendulum and the method of Runge-Kutta that is used to solve the differential equation for the pendulum's motion. I use the same custom animation here, namely by handling the CompositionTarget.Rendering event. The difference here is that I have an additional parameter in my case - the tempo. When the tempo is changed the only parameter in the common case that changes is the length, so we have to find some dependence between the tempo and the length:
In the last equation only the period T is unknown, but we can calculate it on the base of the tempo:
- 60 is the number of seconds in one minute (the tempo is also known as beats per minute)
- 1.935 - in one period we have two beats, so in the beginning I used 2 instead of 1.935 but after a few tests and a few calculations I found out that it should be 1.935, don't make me explain it please.
So now we have the length and everything that we need to implement the Runge-Kutta method. In the handler for the Rendering event of the CompositionTarget we write the following:
private void CompositionTarget_Rendering( object sender, EventArgs e )
{
TimeSpan now = DateTime.Now.TimeOfDay;
TimeSpan diff = now - this.date;
this.dt = ( double )diff.Milliseconds / 1000;
this.date = now;
ODESolver.Function[] f = new ODESolver.Function[ 2 ] { this.F1, this.F2 };
double[] result = ODESolver.RungeKutta4( f, this.xx, this.time, this.dt );
this.Pendulum.SetAngle( 180 * result[ 0 ] / Math.PI );
if ( ( this.thetaN > 0 && result[ 0 ] < 0 ) || ( this.thetaN < 0 && result[ 0 ] > 0 ) )
{
this.TickPlayer.Position = TimeSpan.FromTicks( 0 );
this.TickPlayer.Play();
}
this.thetaN = result[ 0 ];
this.xx = result;
this.time += this.dt;
this.time = Math.Round( this.time, 3 );
}
The result from the equation is an angle. We convert it in degrees and pass it as argument to the SetAngle method of the Pendulum control. Our animation is ready, but that’s not all. As you know, the metronome should play a tick sound every time he passes through the point in which the angle is null. So we check when the angle from the previous calculation and the angle from the present one are positive and negative (this means that the pendulum has passed the null point) and play a tick sound using a MediaElement.
Note: When the browser window that displays the application is not active, the metronome may behave strangely when the window gets active again. The reason for this is that when inactive the CompositionTarget.Rendering event doesn't fire. The same applies also when the Silverlight application gets scrolled out from the visible area.
The other goodies
There are some other things that we should create in order to make the user comfortable when using our metronome. For example, this could be graphic improvements like reflections or other controls that improve the user experience such as play and stop buttons, volume control, indicator for the tempo value.
Reflection
The reflection effect can be accomplished fairly easy using OpacityMask and ScaleTransform. Here is an example:
<Grid x:Name="MetronomeReflection" RenderTransformOrigin="0.5,1" Opacity="0.6">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1" />
</TransformGroup>
</Grid.RenderTransform>
<Grid.OpacityMask>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
<GradientStop Color="#FF000000" Offset="1"/>
<GradientStop Color="#00000000" Offset="0.8"/>
</LinearGradientBrush>
</Grid.OpacityMask>
...
</Grid>
In the OpacityMask's brush we use two colors - black with 100% opacity (the things under the black color are visible) and black with 0% opacity (the things under the transparent color aren't visible). I use GradientBrush to make the effect smoother.
Using a ScaleTransformation with ScaleY set to -1 we transform our control to a mirror image of itself (in the case at the bottom).
The other controls
The other controls implement a pretty simple functionality so I won't explain them here; you can easily take a look at them in the sources. The interesting about them is the PropertyChanged event that is used to sync them with the Metronome control.
Conclusion
That's it! This is a really cool Silverlight control that I'm very pleased with, because it looks fairly nice, it provides the needed functionality and I had a great fun while developing it. I'd like to explain all the things in details but this will emerge into a one big, boring article, full of technical explanations and physical formulas. My goal was to provide you with the main idea of the application, the main steps in creating it and leave a little mystery around it, so everyone can find out the answer of their question by playing with the source or just asking me. Enjoy it! ;)