In This Chapter
• Using the microphone to record audio to isolated storage
• Creating a custom FM radio app
• FM radio region and frequency ranges
• Using the Silverlight Toolkit LoopingSelector
Every Windows Phone device is required to have an FM radio tuner, and likewise, without a microphone, the phone would be no more than a smart camera.
This chapter begins by looking at using the XNA microphone API to record audio. You have some fun with the microphone and create a helium voice app that allows the user to record audio and to modulate the pitch of the recorded audio during playback.
The chapter then looks at the FM radio. You see how to create a custom radio app that uses a Silverlight Toolkit LoopingSelector
to change the radio’s frequency.
The XNA Microphone
class allows you to capture audio data from the phone’s microphone. Microphone
resides in the Microsoft.Xna.Framework.Audio namespace.
To use the Microphone
class you need to perform the following two steps:
1. Add a reference to the Microsoft.Xna.Framework assembly.
2. Create a GameTimer
to pump the XNA FrameworkDispatcher
, discussed next.
XNA components usually rely on initialization of the XNA environment before they can be used, and the Microphone
class is no exception. Creating a GameTimer
to pump the XNA FrameworkDispatcher
usually involves adding some code to your App
class that creates a GameTimer
and subscribes to its FrameAction
event. When creating a Silverlight/XNA hybrid application, this plumbing is automatically put in place when using the “Windows Phone Rich Graphics Application” Visual Studio new project template.
For more information on initializing the XNA environment, see Chapter 20, “Incorporating XNA Graphics in Silverlight.”
Microphone
does not have a public constructor; an instance is retrieved using the static Microphone.Default
property, as shown:
Microphone microphone = Microphone.Default;
Microphone
uses an event driven approach to periodically deliver audio data while it is recording. The Microphone.BufferReady
event is raised as soon as the Microphone
instance’s buffer is full. The size of the buffer is specified using the Microphone.BufferDuration
property, like so:
microphone.BufferDuration = TimeSpan.FromSeconds(1);
The Microphone.GetSampleSizeInBytes
can be used to determine the space requirements for reading the sample data when the BufferReady
event is raised.
int sampleSizeInBytes
= microphone.GetSampleSizeInBytes(microphone.BufferDuration);
This provides the opportunity to create a byte array for storing the sample bytes when the buffer has been filled:
byte[] buffer = new byte[sampleSizeInBytes];
The buffer can later be copied to a stream and saved in isolated storage, as shown:
void HandleBufferReady(object sender, EventArgs e)
{
microphone.GetData(buffer);
stream.Write(buffer, 0, buffer.Length);
}
The next section looks at putting this together to create a page that allows the user to record and play back audio.
They are a dime a dozen, and you may be surprised by just how simple it is to create a helium voice app, which can modulate the pitch of the user’s voice. This section creates a page that allows the user to record audio, which is saved to isolated storage, and to play back the sample with the option to change the pitch of the playback using a slider.
The code for this section is located in the Sensors/Microphone directory of the WindowsPhone7Unleashed.Examples project in the downloadable sample code.
The MicrophoneViewModel
class includes the following two commands:
• ToggleRecordCommand—Starts the microphone recording if not recording; otherwise, recording is stopped and the sample saved to isolated storage.
• PlayCommand—Plays the previously saved sample, if it exists, from isolated storage.
MicrophoneViewModel
initializes the default Microphone
instance to a BufferDuration
of 1 second. A byte
array field is initialized using the microphone’s sample size (see Listing 21.1).
public class MicrophoneViewModel : ViewModelBase
{
readonly Microphone microphone = Microphone.Default;
readonly byte[] buffer;
const string fileName = "MicrophoneSample.bytes";
MemoryStream stream = new MemoryStream();
readonly object fileLock = new object();
public MicrophoneViewModel() : base("microphone")
{
microphone.BufferDuration = TimeSpan.FromSeconds(1);
microphone.BufferReady += HandleBufferReady;
int sampleSizeInBytes = microphone.GetSampleSizeInBytes(
microphone.BufferDuration);
buffer = new byte[sampleSizeInBytes];
toggleRecordCommand = new DelegateCommand(obj => ToggleRecording());
playCommand = new DelegateCommand(obj => Play(), obj => !recording);
}
...
}
The viewmodel’s ToggleRecording
method either calls the StopRecording
or StartRecording
method, depending on the current recording state, as shown:
void ToggleRecording()
{
if (recording)
{
StopRecording();
}
else
{
StartRecording();
}
}
The viewmodel’s StartRecording
method instantiates a new MemoryStream
, which is used to copy the sample data each time the Microphone.BufferReady
event is raised. When the Microphone
object is started, the BufferReady
event is raised after 1 second of recording time. See the following excerpt:
void StartRecording()
{
stream = new MemoryStream();
microphone.Start();
Recording = true;
UpdateCommands();
}
The UpdateCommands
method disables the play command if playback is in progress, as shown:
void UpdateCommands()
{
playCommand.RaiseCanExecuteChanged();
}
The viewmodel’s StopRecording
method tests the state of the microphone. The Microphone.Stop
method is not in the StopRecording
method because doing so would prevent the last BufferReady
event from being raised, which can cause the audio to be cut off. See the following excerpt:
void StopRecording()
{
if (microphone.State != MicrophoneState.Stopped)
{
Recording = false;
UpdateCommands();
}
}
The stop request is, instead, dealt with in the viewmodel’s HandleBufferReady
method, in which the sample data is read into the buffer byte array and then written to a MemoryStream
. If a stop recording request has been made at this point, the stream is flushed, the microphone stopped, and the MemoryStream
is written to isolated storage, as shown:
void HandleBufferReady(object sender, EventArgs e)
{
microphone.GetData(buffer);
stream.Write(buffer, 0, buffer.Length);
if (!recording)
{
stream.Flush();
microphone.Stop();
WriteFile(stream);
}
}
Writing the sample to isolated storage involves retrieving an IsolatedStorageFileStream
and writing the MemoryStream
to it, as shown:
void WriteFile(MemoryStream memoryStream)
{
lock (fileLock)
{
using (var isolatedStorageFile
= IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream fileStream
= isolatedStorageFile.OpenFile(fileName, FileMode.Create))
{
memoryStream.WriteTo(fileStream);
}
}
}
}
Playback of the audio begins with the retrieval of the sample bytes from isolated storage, as shown:
void Play()
{
byte[] bytes = null;
lock (fileLock)
{
using (var userStore
= IsolatedStorageFile.GetUserStoreForApplication())
{
if (userStore.FileExists(fileName))
{
using (IsolatedStorageFileStream fileStream
= userStore.OpenFile(fileName,
FileMode.Open,
FileAccess.Read))
{
bytes = new byte[fileStream.Length];
fileStream.Read(bytes, 0, bytes.Length);
}
}
}
}
if (bytes != null)
{
PlayAudio(bytes);
}
}
An XNA SoundEffect
instance is used to play the sample bytes. The microphone’s sample rate is used to match the sample rate of the SoundEffect
. A SoundEffectInstance
field is used to vary the pitch of the audio during playback. See the following excerpt:
SoundEffectInstance effectInstance;
void PlayAudio(byte[] audioBytes)
{
if (audioBytes == null || audioBytes.Length == 0
|| effectInstance != null
&& effectInstance.State == SoundState.Playing)
{
return;
}
var soundEffect = new SoundEffect(audioBytes,
microphone.SampleRate,
AudioChannels.Mono);
if (effectInstance != null)
{
effectInstance.Dispose();
}
effectInstance = soundEffect.CreateInstance();
effectInstance.Pitch = (float)pitch;
effectInstance.Play();
}
The Pitch
property adjusts the playback of the SoundEffectInstance
during playback, as shown:
double pitch;
public double Pitch
{
get
{
return pitch;
}
set
{
Assign(() => Pitch, ref pitch, value);
if (effectInstance != null)
{
effectInstance.Pitch = (float)value;
}
}
}
The MicrophoneView
page includes a custom AppBar
definition with buttons bound to the commands in the view. The first button is an AppBarToggleButton
, which changes its icon according to the value of the viewmodel’s Recording
property. See the following excerpt:
<u:AppBar>
<u:AppBarToggleButton
Command1="{Binding ToggleRecordCommand}"
Toggled="{Binding Recording}"
Text1="Record"
Text2="Stop"
Icon1Uri="/Sensors/Microphone/Icons/Record.png"
Icon2Uri="/Sensors/Microphone/Icons/StopRecording.png" />
<u:AppBarIconButton
Command="{Binding PlayCommand}"
Text="Play"
IconUri="/Sensors/Microphone/Icons/Play.png" />
</u:AppBar>
The main content panel includes a Slider
that is bound to the viewmodel’s Pitch
property, as shown:
<StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock Text="pitch"
Style="{StaticResource LabelTextIndentedStyle}" />
<Slider Value="{Binding Pitch, Mode=TwoWay}" Minimum="-1" Maximum="1" />
</StackPanel>
By moving the slider to the left the pitch decreases during playback; by moving it to the right the helium voice effect is achieved (see Figure 21.1).
The Microphone
class provides an easy way to capture audio and provides interesting opportunities for adding voice enabled services such as voice recognition and streaming communications.
The FMRadio
class allows you to control the Windows Phone hardware FM radio component. It is a singleton, providing access to a single FMRadio
object via its static Instance
property.
The FMRadio
class has the following four nonstatic properties:
• PowerMode
• Frequency
• CurrentRegion
• SignalStrength
PowerMode
allows you to turn the FM radio device on or off. PowerMode
is of type RadioPowerMode
, an enum with two values: Off and On.
Frequency
is a double property that allows you to control the stations that the radio is tuned to. The set of radio frequency bands available for tuning is determined by the CurrentRegion
property, which is of type RadioRegion
, an enum with the following three values:
• Japan
• UnitedStates
The differences in frequency ranges for each of these regions are as follows
The CurrentRegion
property is used to determine whether a Frequency
value is valid. When setting the Frequency
property, the value must fall within the range as described by the CurrentRegion
property. For example, if the CurrentRegion
property is set to Japan and you attempt to set the Frequency
above 90MHz, an ArgumentException
is raised.
FMRadio.SignalStrength
indicates the reception quality. The FM radio on Windows Phone devices is designed to work with the user’s headphones plugged in, which act as an aerial for the device. It is good practice to inform the user of this fact when a low signal strength is detected. This is demonstrated in the following section.
FMRadio fmRadio = FMRadio.Instance;
fmRadio.PowerMode = RadioPowerMode.On;
fmRadio.CurrentRegion = RadioRegion.UnitedStates;
fmRadio.Frequency = 99.5;
if (fmRadio.SignalStrength < 0.01)
{
MessageBox.Show("This phone uses your headphones "
+ "as an FM radio antenna. "
+ "To listen to radio, connect your headphones.",
"No antenna", MessageBoxButton.OK);
}
To switch off the radio, set the PowerMode
to Off, as shown:
FMRadio.Instance.PowerMode = RadioPowerMode.Off;
This section looks at using the FMRadio
class to create a custom FM radio app that allows the user to switch the radio on and off, to set the region and frequency of the FM radio, and to view the radio’s signal strength. You also look at the Silverlight Toolkit’s LoopSelector
control and how it is used to provide an infinitely scrolling list that allows the user to set the FM radio’s frequency.
The sample code for this section resides in the /Sensors/FMRadio directory of the WindowsPhone7Unleashed.Examples directory in the downloadable sample code.
To begin, the viewmodel’s PoweredOn
property controls the FMRadio
’s PowerMode
property (see Listing 21.2).
Setting the FMRadio
’s Frequency
property causes the SignalStrength
property to be updated immediately. If the Frequency
property is not set, you need to wait momentarily for the SignalStrength
property to be updated. We therefore use another thread to wait a moment before determining whether the user needs to be informed that she should plug in headphones.
public bool PoweredOn
{
get
{
return FMRadio.Instance.PowerMode == RadioPowerMode.On;
}
set
{
bool state = FMRadio.Instance.PowerMode == RadioPowerMode.On;
if (state != value)
{
FMRadio.Instance.PowerMode = value
? RadioPowerMode.On : RadioPowerMode.Off;
if (value)
{
ThreadPool.QueueUserWorkItem(
delegate
{
Wait(2000);
if (SignalStrength < 0.01)
{
MessageService.ShowMessage(
"This phone uses your headphones "
+ "as an FM radio antenna. "
+ "To listen to radio, connect your headphones.",
"No antenna");
}
});
}
OnPropertyChanged(() => PoweredOn);
if (FMRadio.Instance.PowerMode == RadioPowerMode.On)
{
OnPropertyChanged(() => Frequency);
}
}
}
}
The custom Wait
method blocks the thread without resorting to a Thread.Sleep
call. For more information see Listing 15.2, “Wait Method.”
The view contains a ToggleSwitch
that is bound to the viewmodel’s PoweredOn
property, as shown:
<toolkit:ToggleSwitch IsChecked="{Binding PoweredOn, Mode=TwoWay}"
Header="Power" />
The binding is two way, so that when the user taps the switch, the viewmodel property is updated.
The viewmodel’s Frequency
property sets the frequency of the FMRadio
. The minimum allowed frequency is determined using the frequency range for the current region. See the following excerpt:
public double Frequency
{
get
{
return FMRadio.Instance.Frequency < 1
? frequencyMin : FMRadio.Instance.Frequency;
}
set
{
if (Math.Abs(FMRadio.Instance.Frequency - value) >= .1)
{
FMRadio.Instance.Frequency = value;
OnPropertyChanged(() => Frequency);
}
}
}
The viewmodel exposes the RadioRegion
enum values as a property, which is bound to a Silverlight Toolkit ListPicker
in the view, as shown:
<toolkit:ListPicker ItemsSource="{Binding Regions}"
SelectedItem="{Binding Region, Mode=TwoWay}" />
Rather than hard-coding each enum value in the viewmodel, a custom class called EnumUtility
is used to create an IEnumerable<RadioRegion>
(see Listing 21.3).
public static class EnumUtility
{
public static IEnumerable<TEnum> CreateEnumValueList<TEnum>()
{
Type enumType = typeof(TEnum);
IEnumerable<FieldInfo> fieldInfos
= enumType.GetFields().Where(x => enumType.Equals(x.FieldType));
return fieldInfos.Select(
fieldInfo => (TEnum)Enum.Parse(enumType,
fieldInfo.Name, true)).ToList();
}
}
The viewmodel’s Regions
property and its backing field are defined as shown:
readonly IEnumerable<RadioRegion> radioRegions
= EnumUtility.CreateEnumValueList<RadioRegion>();
public IEnumerable<RadioRegion> Regions
{
get
{
return radioRegions;
}
}
The complement of the Regions
property is the viewmodel’s nonplural Region
property, which exposes the FMRadio
’s CurrentRegion
property, as shown:
public RadioRegion Region
{
get
{
return FMRadio.Instance.CurrentRegion;
}
set
{
FMRadio fmRadio = FMRadio.Instance;
if (fmRadio.CurrentRegion != value)
{
fmRadio.CurrentRegion = value;
UpdateFrequencyRanges();
OnPropertyChanged(() => Region);
}
}
}
When the Region
property is changed, the list of valid frequency ranges is updated using the viewmodel’s UpdateFrequencyRangesMethod
.
The Frequency
double value is split into two parts so that its fractional and nonfractional components can be binding sources for two controls in the view; two Silverlight Toolkit LoopingSelector
controls allow the user to select the desired frequency (see Figure 21.2).
For information on the LoopingSelector
, see Chapter 9, “Silverlight Toolkit Controls.”
The radio frequency is split into two using two custom NumericDataSource
objects. The nonfractional segment is defined by the viewmodel field frequenciesNonFractional
, while the fractional component is defined by the field frequenciesFractional
, as shown:
readonly NumericDataSource frequenciesFractional
= new NumericDataSource { Minimum = 0, Maximum = 9 };
The SelectedItem
of the frequenciesFractional
field is set to the fractional component of the current frequency in the viewmodel constructor, like so:
frequenciesFractional.SelectedItem = (int)(Frequency % 1 * 10);
The viewmodel subscribes to each NumericDataSource
object’s SelectChanged
event. When the nonfractional part of the frequency is changed, the handler determines the new frequency by adding the existing fractional part (frequency modulo 1) to the selected value, as shown:
void HandleFrequencyLargeChanged(object sender, SelectionChangedEventArgs e)
{
NumericDataSource dataSource = (NumericDataSource)sender;
var newFrequency = Frequency % 1 + dataSource.SelectedNumber;
if (newFrequency > frequencyMax || newFrequency < frequencyMin)
{
return;
}
Frequency = newFrequency;
}
When the fractional part of the frequency is changed, the handler determines the new frequency by removing the fractional component of the existing frequency and adding it to the selected item, as shown:
void HandleFrequencyFractionPartChanged(
object sender, SelectionChangedEventArgs e)
{
NumericDataSource dataSource = (NumericDataSource)sender;
double f = Frequency;
double newFrequency = f - f % 1 + dataSource.SelectedNumber / 10.0;
if (newFrequency > frequencyMax || newFrequency < frequencyMin)
{
return;
}
Frequency = newFrequency;
}
When the viewmodel is instantiated, or when the Region
property changes, the UpdateFrequencyRanges
method is called, which creates a new NumericDataSource
object for the nonfractional frequency component and sets its Minimum
and Maximum
properties according to the region’s frequency range (see Listing 21.4).
void UpdateFrequencyRanges()
{
if (frequenciesNonFractional != null)
{
frequenciesNonFractional.SelectionChanged -= HandleFrequencyLargeChanged;
}
NumericDataSource dataSource = new NumericDataSource();
switch (Region)
{
case RadioRegion.Europe:
FrequencyMin = 88;
FrequencyMax = 108;
dataSource.Minimum = 88;
dataSource.Maximum = 108;
break;
case RadioRegion.Japan:
FrequencyMin = 76;
FrequencyMax = 90;
dataSource.Minimum = 76;
dataSource.Maximum = 90;
break;
case RadioRegion.UnitedStates:
FrequencyMin = 88.1;
FrequencyMax = 107.9;
dataSource.Minimum = 88;
dataSource.Maximum = 107;
break;
}
double tempFrequency = Frequency;
dataSource.SelectedNumber = (int)(tempFrequency - tempFrequency % 1);
FrequenciesNonFractional = dataSource;
dataSource.SelectionChanged += HandleFrequencyLargeChanged;
}
The viewmodel also indicates the current signal strength of the radio and contains a SignalStrength
property that gets its value directly from the FMRadio
instance, as shown:
public double SignalStrength
{
get
{
return FMRadio.Instance.SignalStrength;
}
}
The device FM radio can also be controlled by the user using the hardware volume buttons, which, when pressed (while the FM radio is powered on) causes an onscreen menu to be displayed. The onscreen menu allows the user to skip forward and backward between stations. The FMRadio
class provides no events for monitoring changes to frequency or signal strength, nor any other property. To monitor changes to the FMRadio
instance, a Timer
is used, which periodically polls the FMRadio
for changes.
It is generally best to avoid the use of timers within your app, as it can unduly degrade battery life. Use a Timer
only if your app absolutely requires the functionality and it is not obtainable by other means.
The Timer
is instantiated in the viewmodel constructor, like so:
timer = new Timer(HandleTimerTick, null, timerIntervalMs, timerIntervalMs);
When the interval elapses, the Timer
instance calls the HandleTimerTick
method. If the previous frequency value is markedly different from the current frequency value, a property changed event for the Frequency
property is raised and the UI updated, as shown:
double previousFrequency;
void HandleTimerTick(object state)
{
OnPropertyChanged(() => SignalStrength);
double frequency = FMRadio.Instance.Frequency;
if (Math.Abs(previousFrequency - frequency) >= 0.1)
{
double fraction = frequency % 1;
Deployment.Current.Dispatcher.BeginInvoke(delegate
{
frequenciesFractional.SelectedNumber = (int)(fraction * 10);
frequenciesNonFractional.SelectedNumber
= (int)(frequency - fraction);
OnPropertyChanged(() => Frequency);
});
}
previousFrequency = frequency;
}
The FMRadioView
page displays the raw frequency value using a TextBlock
. A TextBlock
and a ProgressBar
are used to display the SignalStrength
property (see Listing 21.5).
It is critical to set a number of properties of the LoopingSelector
for it to be displayed correctly, in particular, its ItemSize
, Width
, and Height
.
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel>
<toolkit:ToggleSwitch IsChecked="{Binding PoweredOn, Mode=TwoWay}"
Header="Power" />
<TextBlock Text="frequency" Style="{StaticResource LabelTextStyle}" />
<TextBlock Text="{Binding Frequency}"
Style="{StaticResource ValueTextStyle}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<toolkitPrimitives:LoopingSelector
DataSource="{Binding FrequenciesNonFractional}"
Margin="12" Width="128" Height="128" ItemSize="128,128"
ItemTemplate="{StaticResource LoopNumberTemplate}"
IsEnabled="{Binding PoweredOn}" />
<TextBlock Text="." FontSize="54" VerticalAlignment="Bottom"
FontFamily="{StaticResource PhoneFontFamilySemiBold}" />
<toolkitPrimitives:LoopingSelector
DataSource="{Binding FrequenciesFractional}"
Margin="12" Width="128" Height="128" ItemSize="128,128"
ItemTemplate="{StaticResource LoopNumberTemplate}"
IsEnabled="{Binding PoweredOn}" />
</StackPanel>
<TextBlock Text="region" Style="{StaticResource LabelTextStyle}" />
<toolkit:ListPicker ItemsSource="{Binding Regions}"
SelectedItem="{Binding Region, Mode=TwoWay}" />
<TextBlock Text="signal strength"
Style="{StaticResource LabelTextStyle}" />
<TextBlock Text="{Binding SignalStrength}"
Style="{StaticResource ValueTextStyle}" />
<ProgressBar Value="{Binding SignalStrength}" Maximum="5" />
</StackPanel>
</Grid>
A DataTemplate
called LoopNumberTemplate
is used for presenting the numbers within the LoopingSelectors
. It is defined as a page resource as shown:
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="LoopNumberTemplate">
<Grid>
<TextBlock Text="{Binding}" FontSize="54"
FontFamily="{StaticResource PhoneFontFamilySemiBold}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
Figure 21.3 shows the FMRadioView
page with the radio powered on and the region list expanded. The signal strength field is periodically updated.
The FM radio is a core piece of phone hardware that allows users to listen to audio without having a data connection. The built-in radio app provides the bare essentials for listening, and having programmatic access to the FM radio allows you to create an advanced radio app or to enhance your audio app by including radio capabilities.
This chapter began by looking at using the XNA microphone API to record audio. You had some fun with the microphone and created a helium voice app that allows the user to record audio and to modulate the pitch of the recorded audio during playback.
The chapter then looked at the FM radio and how to create a custom radio app that uses a Silverlight Toolkit LoopingSelector
to change the FM radio’s frequency.
18.191.254.116