Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

Building FM Radio with RDS Support

Rate me:
Please Sign up or sign in to vote.
4.89/5 (18 votes)
8 Jan 2009Ms-PL4 min read 96.9K   3.7K   75   11
This article explains how to build a simple FM radio player with RDS support by using WPF and USBFM library
Image 1

Introduction

This article explains how to use open source USB FM library (written by me) and Windows Presentation Foundation to build a simple yet fully functional radio player with RDS and TMC support.

Background

USB FM library provides managed interfaces, developed with C# to USB FM receivers, supporting RDS. WPF (Windows Presentation Foundation) provides an easy to use framework to build rich user interfaces with zero time investment. "Blending" those together will bring us an ability to build fully functional applications without heavy time investment.

Step 1: Building Wireframes 

In order to build a WPF application, we should first build wireframe. WPF provides us with a rich choice of layout controls. In our case, we'll use Grid to markup areas in the main (and only) application window.

XML
<Grid>
   <Grid.RowDefinitions>
       <RowDefinition Height="*"/>
       <RowDefinition Height="35px"/>
       <RowDefinition Height="Auto"/>
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
       <ColumnDefinition Width="Auto"/>
       <ColumnDefinition Width="*"/>
       <ColumnDefinition Width="Auto"/>
   </Grid.ColumnDefinitions>
<Grid>

As you can see, we have three rows and three columns. Now we can start putting our controls into it.

In any radio receiver, we have jogs to control volume level and tune to stations. There is a ready made jog control, prepared by Microsoft Expression Blend team. So why not use it "as-is".

In order to do this, we have to reference the control library and define a namespace of the control within the XAML file of the application body.

XML
    xmlns:c="clr-namespace:RotaryControl;assembly=RotaryControl"
...
        <c:RotaryControl Name="Volume" RotationIsConstrained="True"  
		ClockwiseMostAngle="340" Angle="340"/>
        <c:RotaryControl Name="Tune" Grid.Column="2"/>

Also we'll add two labels and a listbox of preset stations which will be binded later to FM device library.

XML
 <TextBlock Text="Volume" Grid.Row="1"/>
<TextBlock Text="Tune" Grid.Row="1" Grid.Column="2"/>

<ListBox Name="Presets" ItemTemplate="{StaticResource PresetTemplate}"
Grid.ColumnSpan="3" Grid.Row="2" Background="Transparent"
HorizontalAlignment="Center" >
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <DockPanel Margin="0" IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

The only thing that remains in XAML markup is to set display for frequency and program text indicators, mono/stereo icon and signal strength mitter. In order to set all those, we'll create another grid and put everything inside it.

XML
 <Grid Grid.Column="1">
    <Grid.RowDefinitions>
        <RowDefinition Height="12px"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="20px"/>
        <RowDefinition Height="20px"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width=".2*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <TextBlock Name="Freq" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" 
		Style="{StaticResource LargeTextStyle}"/>
    <TextBlock Name="PS" Grid.Column="1" Grid.Row="2"/>
    <TextBlock Name="PTY" Grid.Column="1" Grid.Row="0" 
	Style="{StaticResource PTYTextStyle}"/>
    <Path Name="MonoStereo" Stroke="White" Fill="White" 
	Stretch="Fill" Grid.Column="1" Grid.Row="0" Width="12" 
	Height="12" HorizontalAlignment="Left"/>
    <Rectangle Grid.RowSpan="4" Fill="{StaticResource SignalBrush}" Margin="10"/>
    <Rectangle Grid.RowSpan="4" Fill="Black" Margin="9" RenderTransformOrigin="0.5,0">
        <Rectangle.RenderTransform>
            <ScaleTransform x:Name="SignalTransform" ScaleX="1"/>
        </Rectangle.RenderTransform>
    </Rectangle>
    <StackPanel Grid.Column="1" Grid.Row="4" HorizontalAlignment="Right" 
	Orientation="Horizontal">
        <TextBlock Style="{StaticResource IndiStyle}" Text="MS" Name="MS"/>
        <TextBlock Style="{StaticResource IndiStyle}" Text="TA" Name="TA"/>
        <TextBlock Style="{StaticResource IndiStyle}" Text="TP" Name="TP"/>
    </StackPanel>
</Grid> 

We are done with wireframing of our application, now it's a good time to make it look better.

Step 2: Styling WPF Application

WPF is not only easy for building UI with markup. It also provides a wide range of styling possibilities. Resources have hierarchical structure, however in our application we'll put all styles and templates in Window.Resource level. First of all, let's set an application wide style for all TextBlocks. 

XML
<Style TargetType="TextBlock">
    <Setter Property="TextAlignment" Value="Center"/>
    <Setter Property="FontFamily" Value="{x:Static SystemFonts.SmallCaptionFontFamily}"/>
    <Setter Property="FontStyle" Value="{x:Static SystemFonts.SmallCaptionFontStyle}"/>
</Style> 

As you can see, when we do not set x:Key property it applies to all resources lower in the hierarchy. Also, we can inherit styles and set special keys to identify resources within XAML markup and code.

XML
<Style x:Key="LargeTextStyle" TargetType="TextBlock">
    <Setter Property="TextAlignment" Value="Center"/>
    <Setter Property="FontSize" Value="50"/>
</Style>
<Style x:Key="PTYTextStyle" TargetType="TextBlock">
    <Setter Property="TextAlignment" Value="Right"/>
    <Setter Property="FontSize" Value="10"/>
</Style> 

Also we can use triggers, which are basic event handlers directly inside styles:

XML
 <Style BasedOn="{StaticResource PTYTextStyle}" x:Key="IndiStyle" TargetType="TextBlock">
    <Setter Property="Margin" Value="5,0,5,0"/>
    <Setter Property="Foreground" Value="White"/>
    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="Foreground" Value="Gray"/>
        </Trigger>
    </Style.Triggers>
</Style> 

In addition to all this, we can completely redefine the look and feel of controls by overriding the Template property like this:

XML
<Style TargetType="Button">
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border Height="25" Width="35" BorderThickness=".5" 
			Background="Black" Name="PART_Border" >
                    <Border.BorderBrush>
                        <LinearGradientBrush EndPoint="0.854,0.854" 
			StartPoint="0.146,0.146">
                            <GradientStop Color="#FF262626" Offset="0"/>
                            <GradientStop Color="#FFD7D7D7" Offset="1"/>
                        </LinearGradientBrush>
                    </Border.BorderBrush>
                    <ContentPresenter HorizontalAlignment="Center" 
			SnapsToDevicePixels="True" Margin="0" 
			MouseLeftButtonDown="Button_MouseLeftButtonDown"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsPressed" Value="True">
                        <Setter Property="BorderBrush" TargetName="PART_Border">
                            <Setter.Value>
                                <LinearGradientBrush EndPoint="0.854,0.854" 
				StartPoint="0.146,0.146">
                                    <GradientStop Color="#FF262626" Offset="1"/>
                                    <GradientStop Color="#FFD7D7D7" Offset="0"/>
                                </LinearGradientBrush>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style> 

But not only styles can be stored inside Resources. We can also share other objects, like geometry (for mono/stereo indicator) or brushes.

XML
<Geometry x:Key="MonoGeometry">M0,0L1,2 2,2 2,4 1,4 0,6z</Geometry>
<Geometry x:Key="StereoGeometry">M0,0L1,2 2,2 3,0 3,6 2,4 1,4 0,6z</Geometry>
<DrawingBrush x:Key="SignalBrush" TileMode="Tile" Viewport="0,0,.3,.1" Stretch="Uniform">
    <DrawingBrush.Drawing>
        <DrawingGroup>
            <GeometryDrawing Brush="Black">
                <GeometryDrawing.Geometry>
                    <RectangleGeometry Rect="0,0,20,20"/>
                </GeometryDrawing.Geometry>
            </GeometryDrawing>
            <GeometryDrawing Brush="White">
                <GeometryDrawing.Geometry>
                    <RectangleGeometry Rect="0,20,20,40"/>
                </GeometryDrawing.Geometry>
            </GeometryDrawing>
        </DrawingGroup>
    </DrawingBrush.Drawing>
</DrawingBrush> 

Also we can define templates for data classes, used in the application. For example, I want the double value of radio preset to appear as button. Here is how to do this:

XML
<DataTemplate x:Key="PresetTemplate">
    <Button Content="{Binding}" />
</DataTemplate> 

Now we are completely done with the UI. It is about time to go towards "code-behind".

Step 3: Wiring Basic Business Logic 

First of all, we have to initialize our USB FM device. This is a very simple task. Just find it:

C#
_device = USBRadioDevice.FindDevice(RadioPlayer.Properties.Settings.Default.PID,
   RadioPlayer.Properties.Settings.Default.VID);

Now we need to subscribe to its events and wire data bindings for some members:

C#
_device.PropertyChanged += (s, ed) => {
   if (ed.PropertyName == "RDS" && _device.RDS != null) { 
      //set bindings
      this.Dispatch(() => {
         Presets.SetBinding(ListBox.ItemsSourceProperty, _device, "Presets");
         Freq.SetBinding(TextBlock.TextProperty, _device, 
		"CurrentFrequency", new ValueConverter<double, 
		double>(d => { return d == 0 ? _device.CurrentStation : d; }));
         PS.SetBinding(TextBlock.TextProperty, _device.RDS, "PS");
         PTY.SetBinding(TextBlock.TextProperty, _device.RDS, "PTYString");
         MonoStereo.SetBinding(Path.DataProperty, _device.RDS, "IsStereo", 
		new ValueConverter<bool, Geometry>(b => 
		{ return (Geometry)(b ? this.Resources["StereoGeometry"] : 
		this.Resources["MonoGeometry"]); }));
         SignalTransform.SetBinding(ScaleTransform.ScaleYProperty, 
		_device.RDS,"SignalStrength", 
		new ValueConverter<byte, double>(b => { return 1-(b / 36d); }));
         MS.SetBinding(TextBlock.IsEnabledProperty, _device.RDS, "HasMS");
         TA.SetBinding(TextBlock.IsEnabledProperty, _device.RDS, "HasTA");
         TP.SetBinding(TextBlock.IsEnabledProperty, _device.RDS, "HasTP");
      });
   }
}; 

In this code, I'm using some "time savers" developed by myself in order to simplify some WPF aspects.:) If you want to learn more about those time savers, visit and subscribe via RSS to my blog.

Now it's a good time to initialize Audio and RDS reports:

C#
_device.InitAudio();
_device.InitRDSReports(); 

In addition, a small trick to be notified about volume and tune knobs "angle" dependency property changed without setting binding explicitly.

C#
Volume.AddValueChanged(RotaryControl.RotaryControl.AngleProperty, (s, ex) => {
   DirectSoundMethods.Volume = (int)Volume.Angle.ToRange
	(Volume.CounterClockwiseMostAngle, Volume.ClockwiseMostAngle, -4000, 0);
});
 Tune.AddValueChanged(RotaryControl.RotaryControl.AngleProperty, (s, ex) => {
   _device.Tune(Tune.Angle > _prevTune);
   _prevTune = Tune.Angle;
}); 

Actually we are done. Now our application is almost ready. The rest is clear and simple.

Step 4: Finalizing the Application 

First of all, we're using platform invoke calls in USB FM library, thus it implements IDisposable interface. If you do not want to leave handlers in memory, it's a very good idea to stop audio, RDS reports thread and dispose USB handler.

C#
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
   _device.StopRDSReports();
   _device.StopAudio();
}
 private void Window_Unloaded(object sender, RoutedEventArgs e) {
   _device.Close();
   _device.Dispose();
   _device = null;
}

Also, our application is running in borderless window, so we have to drag and move it somehow. Why not to use DragMove() method of Windows to achieve this goal?  One note, jog control captures move movement, so if we want it to continue working, it makes sense to learn where mouse move even comes from.

C#
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
    if (e.Source.GetType().IsSubclassOf(typeof(Window)) || 
		e.Source.GetType().Equals(typeof(TextBlock))) DragMove();
} 

The last thing is get preset button click and tune to selected station:

C#
private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
   var button = sender as ContentPresenter;
   if (button != null) {
      var frq = double.Parse(button.Content.ToString());
      _device.Tune(frq);
   }
}

We are done. Now we're ready to compile and run our application. Wasn't it fun? :)

References 

Revision History

  • January 8, 2009 - Published the article

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Architect Better Place
Israel Israel
Hello! My name is Tamir Khason, and I am software architect, project manager, system analyst and [of course] programmer. In addition to writing big amount of documentation, I also write code, a lot of code. I used to work as a freelance architect, project manager, trainer, and consultant here, in Israel, but recently join the company with extremely persuasive idea - to make a world better place. I have very pretty wife and 3 charming kids, but unfortunately almost no time for them.

To be updated within articles, I publishing, visit my blog or subscribe RSS feed. Also you can follow me on Twitter to be up to date about my everyday life.

Comments and Discussions

 
QuestionProblems with Visual Studio 2015 Pin
Member 1305124213-Mar-17 9:49
Member 1305124213-Mar-17 9:49 
General.NET Compact Framework Pin
Wouterrr_16-May-10 23:44
Wouterrr_16-May-10 23:44 
GeneralIs there any way to create a simple VB.Net application using you project Pin
noka0321-Mar-10 0:48
noka0321-Mar-10 0:48 
GeneralProblem with tuning Pin
dhenry_il3-Feb-10 3:36
dhenry_il3-Feb-10 3:36 
QuestionNice but how... Pin
Kenneta14-Jun-09 8:39
Kenneta14-Jun-09 8:39 
GeneralNice Pin
Muigai Mwaura22-May-09 3:38
Muigai Mwaura22-May-09 3:38 
This is really nice you've captured a lot of useful info in one article.
GeneralWhos the man Pin
Sacha Barber9-Jan-09 1:49
Sacha Barber9-Jan-09 1:49 
GeneralRe: Whos the man Pin
Tamir Khason9-Jan-09 2:00
Tamir Khason9-Jan-09 2:00 
GeneralInteresting Pin
Jerry Evans8-Jan-09 14:35
Jerry Evans8-Jan-09 14:35 
GeneralRe: Interesting Pin
Tamir Khason8-Jan-09 19:21
Tamir Khason8-Jan-09 19:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.