Building an FM Radio with RDS Support

Introduction

This article explains how to use the 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

The USB FM library provides managed interfaces, developed with C# to USB FM receivers that support 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 you an ability to build fully functional applications without a heavy time investment.

Step 1: Building Wireframes

To build a WPF application, you should first build a wireframe. WPF provides you with a rich choice of layout controls. In your case, you'll use a Grid to mark up 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, you have three rows and three columns. Now, you can start putting your controls into it.

In any radio receiver, you have jogs to control volume level and tune to stations. There is a ready-made jog control, prepared by the Microsoft Expression Blend team, so you'll use it "as-is."

To do this, you 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, you'll add two labels and a list box of preset stations. These will be bound later to the 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 the XAML markup is to set the display for frequency and program text indicators, mono/stereo icon, and signal strength emitter. To set all those, you'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>

You are finished wireframing your application. Now, it's time to make it look better.

Building an FM Radio with RDS Support

Step 2: Styling the WPF Application

WPF is not just easy for building UI with markup. It also provides wide range of styling possibilities. Resources have a hierarchical structure; however, in your application you'll put all styles and templates in the Window.Resource level. First of all, set the 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 you do not set the x:Key property, it is applied for all resources lower in the hierarchy. Also, you can inherit styles and set special keys to identify resources within the 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, you can use triggers. Triggers 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 additional to all this, you 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>

Styles aren't the only things that can be stored inside Resources. you also can share other objects, such as 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, you can define templates for data classes used in the application. For example, I want to double the value of the radio preset to appear as a button. Here how to do this.

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

Now, you are completely done with the UI. It's time to go toward the "code-behind."

Building an FM Radio with RDS Support

Step 3: Wiring the Basic Business Logic

First of all, you have to initialize your USB FM device. This is very simple task. Just find:

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

Now, you 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" that I developed to simplify some WPF aspects. If you want to learn more about those time savers, visit and subscribe via RSS to my blog.

Now is a good time to initialize the Audio and RDS reports:

_device.InitAudio();
_device.InitRDSReports();

An addition small trick is to be notified about when the 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, you done. Now, your application is almost ready. The rest is clear and simple.

Building an FM Radio with RDS Support

Step 4: Finalizing the Application

First of all, you're using a platform to invoke calls in the USB FM library so that it implements the IDisposable interface. If you do not want to leave handlers in memory, it's a very good idea to stop the audio, RDS reports thread, and dispose of the 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, your application running in a borderless window, so you have to drag and move it somehow. Why not to use the DragMove() Windows method? Note that the jog control is capturing move movement, so if you want it to continue working, it makes sense to learn where the mouse move came 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 to get the preset button click and tune to the 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);
   }
}

You're done. Now, you're ready to compile and run your application. Wasn't it fun?

References

Revision History

  • January, 2009: Initial Publication.


About the Author

Tamir Khason

Hello! My name is Tamir Khason, and I am software architect, project manager, system analyst and [of couse] programmer. In addition to writing big amount of documentation, I also write code, a lot of code. I work as a freelance architect, project manager, trainer, and consultant here, in Israel. I have very pretty wife and 3 charming kids, but unfortunately almost no time for them. If you are in need of my services, please check out my experience, skillsets and rates. I can be reached at tamir[AT]khason.biz or at www.dotNet.us if you have any questions. To be updated within articles, I publishing, visit my blog or subscribe RSS feed.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds