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

  • Today's agile organizations pose operations teams with a tremendous challenge: to deploy new releases to production immediately after development and testing is completed. To ensure that applications are deployed successfully, an automatic and transparent process is required. We refer to this process as Zero Touch Deployment™. This white paper reviews two approaches to Zero Touch Deployment--a script-based solution and a release automation platform. The article discusses how each can solve the key …

  • On-demand Event Event Date: December 18, 2014 The Internet of Things (IoT) incorporates physical devices into business processes using predictive analytics. While it relies heavily on existing Internet technologies, it differs by including physical devices, specialized protocols, physical analytics, and a unique partner network. To capture the real business value of IoT, the industry must move beyond customized projects to general patterns and platforms. Check out this webcast and join industry experts as …

Most Popular Programming Stories

More for Developers

RSS Feeds