Building an FM Radio with RDS Support

Tuesday Jan 27th 2009 by Tamir Khason
Share:

Build a simple FM radio player with RDS support by using WPF and the USBFM library.

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.

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

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.

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.
Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved