Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building FM Radio with RDS Support

0.00/5 (No votes)
8 Jan 2009 1  
This article explains how to build a simple FM radio player with RDS support by using WPF and USBFM library

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.

 <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.

    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.

         <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.

 <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. 

<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.

<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:

 <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:

<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.

<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:

<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:

 _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:

_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:

_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.

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.

  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.

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:

  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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here