Chapter 32. Local FM Radio

image

image

Local FM Radio provides a unique interface to your phone’s built-in FM radio tuner. Unlike the built-in radio app in the Music + Videos hub, this app enables you to directly type the frequency of your desired station. It also shows your current signal strength, which is an interesting little validation of any static you might be experiencing.

The purpose of this app is to demonstrate the phone’s simple but limited radio tuner API: the FMRadio class in the Microsoft.Devices.Radio namespace. Although the functionality exposed is very minimal, it comes with some nice perks, such as automatic integration into the Music + Video hub’s history/now playing lists.

The User Interface

As you can see from the screenshot to the right, this app’s user interface is a cross between Tip Calculator and Alarm Clock. Listing 32.1 contains the XAML for this app’s one and only page.

Listing 32.1 MainPage.xaml—The User Interface for Local FM Radio


<phone:PhoneApplicationPage
    x:Class="WindowsPhoneApp.MainPage" x:Name="Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:local="clr-namespace:WindowsPhoneApp"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" shell:SystemTray.IsVisible="True">

  <phone:PhoneApplicationPage.Resources>
    <!-- Style for calculator buttons -->
    <Style x:Key="CalculatorButtonStyle" TargetType="Button">
      <Setter Property="FontSize" Value="36"/>
      <Setter Property="FontFamily"
              Value="{StaticResource PhoneFontFamilySemiLight}"/>
      <Setter Property="BorderThickness" Value="0"/>
      <Setter Property="Width" Value="132"/>
      <Setter Property="Height" Value="108"/>
    </Style>
  </phone:PhoneApplicationPage.Resources>

  <StackPanel Style="{StaticResource PhoneTitlePanelStyle}">
    <TextBlock Text="FM RADIO"
                 Style="{StaticResource PhoneTextTitle0Style}"/>

    <!-- A user control much like the time display from Alarm Clock -->
    <local:FrequencyDisplay x:Name="FrequencyDisplay" Margin="0,48"
                            HorizontalAlignment="Center" FontSize="220"/>

    <!-- The same calculator buttons from Tip Calculator, but with a
         power button instead of a 00 button -->
    <Canvas x:Name="ButtonPanel" Height="396" Margin="-24,0">
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="7" Canvas.Left="-6" Canvas.Top="-1"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="8" Canvas.Left="114" Canvas.Top="-1"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="9" Canvas.Left="234" Canvas.Top="-1"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="4" Canvas.Top="95" Canvas.Left="-6"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="5" Canvas.Top="95" Canvas.Left="114"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="6" Canvas.Top="95" Canvas.Left="234"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="1" Canvas.Top="191" Canvas.Left="-6"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="2" Canvas.Top="191" Canvas.Left="114"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="3" Canvas.Top="191" Canvas.Left="234"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="0" Canvas.Top="287" Canvas.Left="-6"/>
      <Button Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorMainBrush, ElementName=Page}"
                Content="power" Width="252" Canvas.Top="287" Canvas.Left="114"/>
      <Button Style="{StaticResource CalculatorButtonStyle}" FontSize="32"
                FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                Background="{Binding CalculatorSecondaryBrush, ElementName=Page}"
                Content="C" Height="204" Canvas.Top="-1" Canvas.Left="354"/>
      <Button x:Name="BackspaceButton" Height="204"
                Style="{StaticResource CalculatorButtonStyle}"
                Background="{Binding CalculatorSecondaryBrush, ElementName=Page}"
                Canvas.Top="191" Canvas.Left="354">
        <!-- The "X in an arrow" backspace drawing -->
        <Canvas Width="48" Height="32">
          <Path x:Name="BackspaceXPath" Data="M24,8 39,24 M39,8 24,24"
                  Stroke="{StaticResource PhoneForegroundBrush}"
                  StrokeThickness="4"/>
          <Path x:Name="BackspaceBorderPath" StrokeThickness="2"
                  Data="M16,0 47,0 47,31 16,31 0,16.5z"
                  Stroke="{StaticResource PhoneForegroundBrush}"/>
        </Canvas>
      </Button>
    </Canvas>
    <!-- A signal strength display -->
    <TextBlock x:Name="SignalStrengthTextBlock" Margin="24"
               HorizontalAlignment="Center" />
  </StackPanel>
</phone:PhoneApplicationPage>


Notes:

→ The button style in the page’s resources collection and the corresponding canvas with buttons is exactly like the XAML from Chapter 10’s Tip Calculator, except that the double-zero button has been replaced with a power button.

→ The frequency display with the custom seven-segment font is implemented with a FrequencyDisplay user control. It is almost identical to—but simpler than—the TimeDisplay user control in Chapter 20, “Alarm Clock.” It is not shown in this chapter, but you can view it with this app’s source code.

The Code-Behind

Listing 32.2 contains the code-behind for Local FM Radio.

Listing 32.2 MainPage.xaml.cs—The Code-Behind for Local FM Radio


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Microsoft.Devices.Radio;
using Microsoft.Phone.Controls;

namespace WindowsPhoneApp
{
  public partial class MainPage : PhoneApplicationPage
  {
    double frequency;
    DateTime? lastDigitButtonTap;

    // Two theme-specific custom brushes
    public Brush CalculatorMainBrush { get; set; }
    public Brush CalculatorSecondaryBrush { get; set; }

    public MainPage()
    {
      InitializeComponent();

      // Ensure the radio is on
      StartRadio();

      // Work around having no radio events by polling
      DispatcherTimer timer = new DispatcherTimer {
        Interval = TimeSpan.FromSeconds(2) };
      timer.Tick += delegate(object sender, EventArgs e) {
        // Update the signal strength every two seconds
        this.SignalStrengthTextBlock.Text = "signal strength: " +
             (FMRadio.Instance.SignalStrength * 100).ToString("##0");
      };
      timer.Start();

      // A single handler for all calculator button taps
      this.ButtonPanel.AddHandler(Button.MouseLeftButtonUpEvent,
        new MouseButtonEventHandler(CalculatorButton_MouseLeftButtonUp),
        true /* handledEventsToo */);

      // Handlers to ensure that the backspace button's vector content changes
      // color appropriately when the button is pressed
      this.BackspaceButton.AddHandler(Button.MouseLeftButtonDownEvent,
        new MouseButtonEventHandler(BackspaceButton_MouseLeftButtonDown),
        true /* handledEventsToo */);
      this.BackspaceButton.AddHandler(Button.MouseLeftButtonUpEvent,
        new MouseButtonEventHandler(BackspaceButton_MouseLeftButtonUp),
        true /* handledEventsToo */);
      this.BackspaceButton.MouseMove += BackspaceButton_MouseMove;

      // Set the colors of the two custom brushes based on whether
      // we're in the light theme or dark theme
      if ((Visibility)Application.Current.Resources["PhoneLightThemeVisibility"]
                        == Visibility.Visible)
      {
        this.CalculatorMainBrush = new SolidColorBrush(
                                         Color.FromArgb(0xFF, 0xEF, 0xEF, 0xEF));
        this.CalculatorSecondaryBrush = new SolidColorBrush(
                                         Color.FromArgb(0xFF, 0xDE, 0xDF, 0xDE));
      }
      else
      {
        this.CalculatorMainBrush = new SolidColorBrush(
                                         Color.FromArgb(0xFF, 0x18, 0x1C, 0x18));
        this.CalculatorSecondaryBrush = new SolidColorBrush(
                                         Color.FromArgb(0xFF, 0x31, 0x30, 0x31));
      }

      // Grab the current frequency from the device's radio
      this.frequency = FMRadio.Instance.Frequency;
      UpdateFrequencyDisplay();
    }

    void StartRadio()
    {
      try
      {
        // This would throw a RadioDisabledException if the app weren't given
        // the ID_CAP_MEDIALIB capability, but we're worried instead about an
        // UnauthorizedAccessException thrown when the phone is connected to Zune
        FMRadio.Instance.PowerMode = RadioPowerMode.On;
      }
      catch
      {
        // Show a message explaining the limitation while connected to Zune
        MessageBox.Show("Be sure that your phone is disconnected from your " +
          "computer.", "Cannot turn radio on", MessageBoxButton.OK);
        return;
      }

      if (FMRadio.Instance.SignalStrength == 0)
      {
        // Show a message similar to the built-in radio app
        MessageBox.Show("This phone uses your headphones as an FM radio " +
          "antenna. To listen to radio, connect your headphones.", "No antenna",
          MessageBoxButton.OK);
      }
    }

    void StopRadio()
    {
      try { FMRadio.Instance.PowerMode = RadioPowerMode.Off; }
      catch {} // Ignore exception from being connected to Zune
    }

    // A single handler for all calculator button taps
    void CalculatorButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
      // Although sender is the canvas, the OriginalSource is the tapped button
      Button button = e.OriginalSource as Button;
      if (button == null)
        return;

      string content = button.Content.ToString();

      // Determine what to do based on the string content of the tapped button
      double digit;
      if (content == "power")
      {
        if (FMRadio.Instance.PowerMode == RadioPowerMode.On)
          StopRadio();
        else
          StartRadio();
      }
      else if (double.TryParse(content, out digit)) // double so division works
      {
        // If there are already four digits (including the decimal place), or if
        // the user hasn't recently typed digits, clear the frequency first
        if (this.frequency > 100 || this.lastDigitButtonTap == null ||
            DateTime.Now - this.lastDigitButtonTap > TimeSpan.FromSeconds(3))
          this.frequency = 0;

        // Append the digit
        this.frequency *= 10;
        this.frequency += digit / 10;

        this.lastDigitButtonTap = DateTime.Now;
      }
      else if (content == "C")
      {
        // Clear the frequency
        this.frequency = 0;
      }
      else // The backspace button
      {
        // Chop off the last digit (the decimal place) with a cast
        int temp = (int)this.frequency;
        // Shift right by 1 place
        this.frequency = (double)temp / 10;
      }
      UpdateFrequencyDisplay();
    }

    void UpdateFrequencyDisplay()
    {
      try
      {
        this.FrequencyDisplay.Foreground =
          Application.Current.Resources["PhoneAccentBrush"] as Brush;

        // Update the display
        this.FrequencyDisplay.Frequency = this.frequency;
        // Update the radio
        FMRadio.Instance.Frequency = this.frequency;
      }
      catch
      {
        if (FMRadio.Instance.PowerMode == RadioPowerMode.On)
        {
          // Caused by an invalid frequency value, which easily
          // happens while typing a valid frequency
          this.FrequencyDisplay.Foreground = new SolidColorBrush(Colors.Red);
        }
      }
    }

    // Change the color of the two paths inside the backspace button when pressed
    void BackspaceButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
      this.BackspaceXPath.Stroke =
        Application.Current.Resources["PhoneBackgroundBrush"] as Brush;
      this.BackspaceBorderPath.Stroke =
        Application.Current.Resources["PhoneBackgroundBrush"] as Brush;
    }

    // Change the color of the two paths back when no longer pressed
    void BackspaceButton_MouseLeftButtonUp(object sender, MouseEventArgs e)
    {
      this.BackspaceXPath.Stroke =
        Application.Current.Resources["PhoneForegroundBrush"] as Brush;
      this.BackspaceBorderPath.Stroke =
        Application.Current.Resources["PhoneForegroundBrush"] as Brush;
    }
    // Workaround for when the finger has not yet been released but the color
    // needs to change back because the finger is no longer over the button
    void BackspaceButton_MouseMove(object sender, MouseEventArgs e)
    {
      // this.BackspaceButton.IsMouseOver lies when it has captured the mouse!
      // Use GetPosition instead:
      Point relativePoint = e.GetPosition(this.BackspaceButton);
      // We can get away with this simple check because
      // the button is in the bottom-right corner
      if (relativePoint.X < 0 || relativePoint.Y < 0)
        BackspaceButton_MouseLeftButtonUp(null, null); // Not over the button
      else
        BackspaceButton_MouseLeftButtonDown(null, null); // Still over the button
    }
  }
}


Notes:

→ Most of the code is identical to the code from Chapter 10’s Tip Calculator. Besides the code that interacts with the FMRadio class and the code that handles tapping the power button, the main difference is that only one spot to the right of the decimal is maintained for the frequency value, compared to two spots for Tip Calculator’s amount value.

→ You can get an instance of the FMRadio class via the static FMRadio.Instance property. The instance exposes three read-write properties that control the phone’s single radio tuner exposed to all apps:

Frequency, a double value representing the current radio station.

PowerMode, which is either On or Off.

CurrentRegion, which can be UnitedStates, Japan, or Europe. The latter really means “every place in the world that isn’t the U.S. or Japan.”

It also exposes a read-only SignalStrength double property that exposes the received signal strength indicator (RSSI). The range of this value has not been documented, but it appears to be a value from 0 (no signal) to 1 (best signal), at least on the hardware I’ve tested.

→ Because the signal strength can constantly vary, but there are no radio-specific events exposed, MainPage’s constructor uses a timer to refresh the signal strength display every two seconds. Although this kind of polling is bad for battery life, it’s unlikely that a user will be running this app for very long. That’s because the radio can keep playing after the app has been exited (and, importantly, this app does not run under the lock screen). The constructor also initializes the frequency variable to whatever frequency the radio was previously set.

→ The StartRadio and StopRadio methods toggle the value of the PowerMode property. StartRadio also shows a message similar to what the built-in radio app displays if the signal strength is zero, as shown in Figure 32.1. This app assumes that the user’s headphones must not be plugged in when this happens, as the headphones act as the FM antenna for every current phone model.

Figure 32.1 When the phone’s headphones are not connected, the phone cannot pick up any FM signal.

image

→ When the radio is on, setting the frequency to an invalid value throws an exception. Valid and invalid values are based on the current region, which would be complicated to detect with your own logic. Therefore, this app takes the easy way out and leverages the exception to turn the frequency display red. (Of course, if the current accent color is already red, then this behavior is not noticeable.)

→ After the user has left the app (or while the app is still running), he/she can still control the radio by tapping the volume-up or volume-down button. This brings up the top overlay shown in Figure 32.2. Interestingly, this overlay enables seeking to the previous/next valid station (with the rewind and fast-forward buttons), so the current station can get out-of-sync with the app’s own frequency display if this is done while Local FM Radio is running. Although this app could attempt to detect and correct this situation inside its timer’s Tick event handler, this is a minor-enough issue to warrant leaving things as-is.

Figure 32.2 In addition to changing the volume, the volume control overlay enables starting/stopping the radio, and even seeking to the previous or next frequency with a strong-enough signal.

image

The Finished Product

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.106.100