Blograby

Sound Recorder (Saving Audio Files & Playing Sound Backward)

Sound Recorder enables you to record, manage, and play audio clips. It is named after the Sound Recorder program on Windows and reminiscent of the Voice Memos app on the iPhone. It can come in quite handy when you’re away from a computer and have some thoughts that you don’t want to forget, especially because it enables you to pause in the midst of a single recording.

Control your recording with simple (and large!) record, pause, and stop buttons. Rename or delete previous recordings one-by-one, or bulk-delete unwanted recordings with a check box mechanism matching the one used by the builtin Mail app. When playing a recording, you can adjust the playback speed, pause it, adjust the playback position on an interactive slider, and even reverse the sound!

The idea of adjusting the speed is that you can listen to recorded thoughts or a lecture much faster than the words were originally spoken. Playing the audio back at a faster rate can help you be more productive.

But why would you want to play recorded words backward? Laura Foy from Microsoft’s Channel 9 has theorized that it’s to “find out if you’re secretly sending satanic messages” (see http://bit.ly/laurafoy), but my real motivation is to enable people to play the nerdy game my brother and I used to play as kids. Here’s how you play:

  1. Record yourself saying a word or phrase.
  2. Play it backwards many times, so you can try to memorize what it sounds like backward.
  3. Make a new recording with you mimicking the backward audio.
  4. Play this new recording backward to see how close you can come to replicating the original word or phrase.

We used to play this game with Sound Recorder on Windows (the good version of the program, prior to Windows Vista). Now you can play it anytime and anywhere with Sound Recorder on your Windows Phone! You’ll be surprised by the sounds you have to make to produce a good result!

As far as interaction with the microphone is concerned, Sound Recorder is simpler than Talking Parrot because it doesn’t need to automatically determine when to start and stop collecting data. This app requires a lot more code, however, for managing the audio that it does capture.

Sound Recorder contains three pages in addition to its about page: the main page, which does all the recording; the list page, which shows past recordings; and the details page, which handles playback and editing.

The Main Page

The main page, shown at the beginning of this chapter, has three basic states: stopped, recording, and paused. Figure 36.1 demonstrates all three.

FIGURE 36.1 The three possible states of the main page.

The four buttons (shown two at a time) mimic application bar buttons but are significantly bigger. Using real application bar buttons would be fine (and easier to implement), but this makes the buttons a little easier to press when the user is in a hurry.

This page’s user interface, with its photograph and Volume Units (VU) meter, does a bad job of following the design guideline that Windows Phone apps should be “authentically digital.” Showing a digital display similar to the phone’s voice recognition overlay would fit in better. However, sometimes violating guidelines can help your app stand out in a positive way.

The User Interface

Listing 36.1 contains the XAML for the main page. It consists of two images, four custom buttons, a line for the VU meter needle, and a text block for displaying the elapsed time.

LISTING 36.1 MainPage.xaml—The User Interface for Sound Recorder’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
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”>
<Canvas>
<!– The on-air image –>
<Image Source=”Images/background.png”/>
<!– The off-air image –>
<Image x:Name=”OffAirImage” Source=”Images/offAir.png”/>
<!– The large buttons: 2 in the same left spot, 2 in the same right spot –>
<local:ImageButton x:Name=”RecordButton” Click=”RecordButton_Click”
Text=”record” Canvas.Left=”16” Canvas.Top=”586”
Source=”../../Images/RecordButton.png”
PressedSource=”../../Images/RecordButtonPressed.png”/>
<local:ImageButton x:Name=”PauseButton” Click=”PauseButton_Click”
Text=”pause” Canvas.Left=”16” Canvas.Top=”586”
Source=”../../Images/PauseButton.png”
PressedSource=”../../Images/PauseButtonPressed.png”
Visibility=”Collapsed”/>
<local:ImageButton x:Name=”ListButton” Click=”ListButton_Click”
Text=”list” Canvas.Left=”371” Canvas.Top=”586”
Source=”../../Images/ListButton.png”
PressedSource=”../../Images/ListButtonPressed.png”/>
<local:ImageButton x:Name=”StopButton” Click=”StopButton_Click”
Text=”stop” Canvas.Left=”371” Canvas.Top=”586”
Source=”../../Images/StopButton.png”
PressedSource=”../../Images/StopButtonPressed.png”
Visibility=”Collapsed”/>
<!– The needle for the sound meter –>
<Line Canvas.Left=”240” Canvas.Top=”590” Width=”3” Height=”110” Y2=”110”
Stroke=”Black” StrokeThickness=”3” StrokeStartLineCap=”Triangle”
RenderTransformOrigin=”.5,1”>
<Line.RenderTransform>
<RotateTransform x:Name=”NeedleTransform” Angle=”-55”/>
</Line.RenderTransform>
</Line>
<!– The elapsed time –>
<TextBlock x:Name=”TimerTextBlock” Canvas.Top=”512” Width=”480”
TextAlignment=”Center” Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”White” Visibility=”Collapsed”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

FIGURE 36.2 The overlay image replaces the photo and dims the sound meter.

The Code-Behind

Listing 36.2 contains the code-behind for the main page.

LISTING 36.2 MainPage.xaml.cs—The Code-Behind for Sound Recorder’s Main Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Used for capturing audio from the microphone
byte[] buffer;
MemoryStream stream;
// Needle management
double targetNeedleAngle;
const int MIN_ANGLE = -55;
const int MAX_ANGLE = 55;
const int VELOCITY_FACTOR = 10;
const int DOWNWARD_VELOCITY = -6;
const int SMALL_ANGLE_DELTA = 6;
const int RANGE_FACTOR = 20;
// The current state (Stopped, Recording, or Paused)
AudioState currentState = AudioState.Stopped;
public MainPage()
{
InitializeComponent();
CompositionTarget.Rendering += CompositionTarget_Rendering;
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
// Configure the microphone with the smallest supported BufferDuration (.1)
Microphone.Default.BufferDuration = TimeSpan.FromSeconds(.1);
Microphone.Default.BufferReady += Microphone_BufferReady;
// Initialize the buffer for holding microphone data
int size = Microphone.Default.GetSampleSizeInBytes(
Microphone.Default.BufferDuration);
this.buffer = new byte[size];
// Initialize the stream used to record microphone data
this.stream = new MemoryStream();
// Listen the whole time so the needle moves even when not recording
Microphone.Default.Start();
}
void Microphone_BufferReady(object sender, EventArgs e)
{
int size = Microphone.Default.GetData(this.buffer);
if (size == 0)
return;
// Calculate the target angle for the volume meter needle
long volume = GetAverageVolume(size);
double range = Math.Min(MAX_ANGLE – MIN_ANGLE, volume / RANGE_FACTOR);
this.targetNeedleAngle = MIN_ANGLE + range;
if (CurrentState == AudioState.Recording)
{
// If recording, write the current buffer to the stream and
// refresh the elapsed time
this.stream.Write(this.buffer, 0, size);
TimeSpan recordingLength = Microphone.Default.GetSampleDuration(
(int)this.stream.Position);
this.TimerTextBlock.Text = String.Format(“{0:00}:{1:00}”,
recordingLength.Minutes, recordingLength.Seconds);
}
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
double newAngle = this.targetNeedleAngle;
double delta = this.targetNeedleAngle – this.NeedleTransform.Angle;
// If the difference is larger than SMALL_ANGLE_DELTA°, gradually move the
// needle rather than directly setting its angle to the target angle
if (Math.Abs(delta) > SMALL_ANGLE_DELTA)
{
// Limit the downward velocity, so it returns to the
// resting position at a constant rate (DOWNWARD_VELOCITY)
newAngle = this.NeedleTransform.Angle +
Math.Max(delta / VELOCITY_FACTOR, DOWNWARD_VELOCITY);
}
// Update the needle’s angle, restricting it
// to a range of MIN_ANGLE° to MAX_ANGLE°
this.NeedleTransform.Angle =
Math.Max(MIN_ANGLE, Math.Min(MAX_ANGLE, newAngle));
}
// Returns the average value among all the values in the buffer
int GetAverageVolume(int numBytes)
{
long total = 0;
// Buffer is an array of bytes, but we want to examine each 2-byte value
for (int i = 0; i < numBytes; i += 2)
{
// Cast from short to int to prevent -32768 from overflowing Math.Abs
int value = Math.Abs((int)BitConverter.ToInt16(this.buffer, i));
total += value;
}
return (int)(total / (numBytes / 2));
}
AudioState CurrentState
{
get { return this.currentState; }
set
{
this.currentState = value;
// Not pretty code, but shorter than the alternatives
switch (this.currentState)
{
case AudioState.Recording:
RecordButton.Visibility = Visibility.Collapsed;
ListButton.Visibility = Visibility.Collapsed;
OffAirImage.Visibility = Visibility.Collapsed;
PauseButton.Visibility = Visibility.Visible;
StopButton.Visibility = Visibility.Visible;
TimerTextBlock.Text = “”;
TimerTextBlock.Visibility = Visibility.Visible;
break;
case AudioState.Paused:
RecordButton.Visibility = Visibility.Visible;
OffAirImage.Visibility = Visibility.Visible;
PauseButton.Visibility = Visibility.Collapsed;
TimerTextBlock.Text += “ (paused)”;
break;
case AudioState.Stopped:
RecordButton.Visibility = Visibility.Visible;
ListButton.Visibility = Visibility.Visible;
OffAirImage.Visibility = Visibility.Visible;
PauseButton.Visibility = Visibility.Collapsed;
StopButton.Visibility = Visibility.Collapsed;
TimerTextBlock.Visibility = Visibility.Collapsed;
break;
}
}
}
// Button click handlers
void RecordButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Recording;
}
void ListButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Stopped;
this.NavigationService.Navigate(
new Uri(“/ListPage.xaml”, UriKind.Relative));
}
void PauseButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Paused;
}
void StopButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Stopped;
// Create a new recording with a unique filename
Recording r = new Recording { Filename = Guid.NewGuid().ToString(),
TimeStamp = DateTimeOffset.Now };
// Save the recording
r.SaveContent(this.stream);
// Ready the stream for another recording
this.stream.Position = 0;
// Add the recording to the persisted list
Settings.RecordingsList.Value.Add(r);
}
}
}

[/code]

LISTING 36.3 Recording.cs—The Object Representing Each Sound File Stored in Isolated Storage

[code]

using System;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public class Recording : INotifyPropertyChanged
{
// The backing fields
string filename;
string label;
DateTimeOffset timeStamp;
TimeSpan duration;
// The properties, which raise change notifications
public string Filename
{
get { return this.filename; }
set { this.filename = value; OnPropertyChanged(“Filename”); }
}
public string Label
{
get { return this.label; }
set { this.label = value; OnPropertyChanged(“Label”);
// Raise notifications for the readonly properties based on Label
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
public DateTimeOffset TimeStamp
{
get { return this.timeStamp; }
set { this.timeStamp = value; OnPropertyChanged(“TimeStamp”);
// Raise notifications for the readonly properties based on TimeStamp
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
public TimeSpan Duration
{
get { return this.duration; }
set { this.duration = value; OnPropertyChanged(“Duration”);
// Raise notifications for the readonly properties based on Duration
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
// A few computed properties for display purposes
public string Title
{
get {
return String.Format(“{0} ({1:00}:{2:00})”,
this.label ?? this.TimeStamp.LocalDateTime.ToShortTimeString(),
this.Duration.Minutes, Math.Floor(this.Duration.Seconds));
}
}
public string ShortTitle
{
get {
return this.label ?? this.TimeStamp.LocalDateTime.ToShortTimeString();
}
}
public string Subtitle
{
get {
if (this.label != null)
return String.Format(“{0} {1}”,
this.TimeStamp.LocalDateTime.ToShortDateString(),
this.TimeStamp.LocalDateTime.ToShortTimeString());
else
return this.TimeStamp.LocalDateTime.ToShortDateString();
}
}
// Save the stream to isolated storage
public void SaveContent(MemoryStream memoryStream)
{
// Store the duration of the content, used for display purposes
this.Duration = Microphone.Default.GetSampleDuration(
(int)memoryStream.Position);
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.CreateFile(this.Filename))
{
stream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Position);
}
}
// Get the raw bytes from the file in isolated storage
byte[] GetBuffer()
{
byte[] buffer =
new byte[Microphone.Default.GetSampleSizeInBytes(this.Duration)];
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open))
{
stream.Read(buffer, 0, buffer.Length);
}
return buffer;
}
// Create and return a sound effect based on the raw bytes in the file
public SoundEffect GetContent()
{
return new SoundEffect(this.GetBuffer(), Microphone.Default.SampleRate,
AudioChannels.Mono);
}
// Delete the file
public void DeleteContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
userStore.DeleteFile(this.Filename);
}
// Overwrite the file’s contents with the audio data reversed
public void Reverse()
{
byte[] buffer = this.GetBuffer();
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open, FileAccess.Write))
{
// Reverse each 2-byte chunk (each 16-bit audio sample)
for (int i = buffer.Length – 2; i >= 0; i -= 2)
stream.Write(buffer, i, 2);
}
}
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

The List Page

The list page, shown in Figure 36.3, contains a list box with recordings that link to the details page. However, this is not a regular list box—it is a custom subclass called CheckableListBox that mimics the Mail app’s mechanism for bulk-selecting items. To enter bulk-selection mode, you can either tap the application bar button or tap the leftmost edge of any item in the list. The latter approach has the advantage of automatically selecting the tapped item. The code to CheckableListBox is not covered in this chapter, but it is included with this chapter’s source code.

FIGURE 36.3 The CheckableListBox supports multi-select interaction the same way as the phone’s built-in Mail app.

The only thing you can do with bulk-selected items is delete them. Notice in Figure 36.3 that the application bar changes to show a delete button at the same time that the check boxes appear.

The XAML for the list page is shown in Listing 36.4, and the code-behind is shown in Listing 36.5.

LISTING 36.4 ListPage.xaml—The User Interface for Sound Recorder’s List of Recordings

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.ListPage”
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=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with one button and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”select” Click=”SelectButton_Click”
IconUri=”/Shared/Images/appbar.select.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SOUND RECORDER”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”recordings” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock x:Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No recordings”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– A list box supporting check boxes for bulk selection –>
<local:CheckableListBox x:Name=”CheckableListBox” Grid.Row=”1”
Margin=”0,18,0,0”
SelectionMode=”Multiple” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<local:CheckableListBox.ItemTemplate>
<DataTemplate>
<!– Give each recording two lines: a title and a subtitle –>
<StackPanel>
<TextBlock Text=”{Binding Title}” Margin=”-2,-13,0,0”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Text=”{Binding Subtitle}” Margin=”0,-5,0,28”
Style=”{StaticResource PhoneTextSubtleStyle}”/>
</StackPanel>
</DataTemplate>
</local:CheckableListBox.ItemTemplate>
</local:CheckableListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 36.5 ListPage.xaml.cs—The Code-Behind for Sound Recorder’s List of Recordings

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class ListPage : PhoneApplicationPage
{
bool inSelectMode;
public ListPage()
{
InitializeComponent();
// Assign the data source for the list box
this.DataContext = Settings.RecordingsList.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (Settings.RecordingsList.Value.Count == 0)
ShowListAsEmpty();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// The Back button should exit select mode
if (this.inSelectMode)
{
e.Cancel = true;
LeaveSelectMode();
}
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.CheckableListBox.SelectedItems.Count == 1 &&
!this.CheckableListBox.AreCheckBoxesShowing)
{
// This is a normal, single selection, so navigate to the details page
Settings.SelectedRecordingIndex.Value =
this.CheckableListBox.SelectedIndex;
this.NavigationService.Navigate(
new Uri(“/DetailsPage.xaml”, UriKind.Relative));
// Clear the selection for next time
this.CheckableListBox.SelectedIndex = -1;
}
else if (this.CheckableListBox.AreCheckBoxesShowing && !this.inSelectMode)
this.EnterSelectMode();
else if (!this.CheckableListBox.AreCheckBoxesShowing && this.inSelectMode)
this.LeaveSelectMode();
if (this.inSelectMode)
(this.ApplicationBar.Buttons[0] as IApplicationBarIconButton).IsEnabled =
(this.CheckableListBox.SelectedItems.Count > 0);
}
void ShowListAsEmpty()
{
NoItemsTextBlock.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void EnterSelectMode()
{
// Show the check boxes
this.CheckableListBox.ShowCheckBoxes();
// Clear the application bar and show a delete button
this.ApplicationBar.Buttons.Clear();
ApplicationBarIconButton deleteButton = new ApplicationBarIconButton(
new Uri(“/Shared/Images/appbar.delete.png”, UriKind.Relative));
deleteButton.Text = “delete”;
deleteButton.IsEnabled = false; // Will be enabled when >=1 item selected
deleteButton.Click += DeleteButton_Click;
this.ApplicationBar.Buttons.Add(deleteButton);
this.inSelectMode = true;
}
void LeaveSelectMode()
{
// Hide the check boxes
if (this.CheckableListBox.AreCheckBoxesShowing)
this.CheckableListBox.HideCheckBoxes();
// Clear the application bar and show a select button
this.ApplicationBar.Buttons.Clear();
ApplicationBarIconButton button = new ApplicationBarIconButton(
new Uri(“/Shared/Images/appbar.select.png”, UriKind.Relative));
button.Text = “select”;
button.Click += SelectButton_Click;
this.ApplicationBar.Buttons.Add(button);
this.inSelectMode = false;
}
// Application bar handlers
void SelectButton_Click(object sender, EventArgs e)
{
EnterSelectMode();
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete “ +
(this.CheckableListBox.SelectedItems.Count > 1 ? “these recordings” :
“this recording”) + “?”, “Delete recording” +
(this.CheckableListBox.SelectedItems.Count > 1 ? “s” : “”) + “?”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Recording[] itemsToDelete =
new Recording[this.CheckableListBox.SelectedItems.Count];
this.CheckableListBox.SelectedItems.CopyTo(itemsToDelete, 0);
this.CheckableListBox.SelectedIndex = -1;
this.LeaveSelectMode();
for (int i = 0; i < itemsToDelete.Length; i++)
{
// Remove it from the list
Settings.RecordingsList.Value.Remove(itemsToDelete[i]);
// Delete the audio file in isolated storage
itemsToDelete[i].DeleteContent();
}
if (Settings.RecordingsList.Value.Count == 0)
ShowListAsEmpty();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Sound Recorder”, UriKind.Relative));
}
}
}

[/code]

The Details Page

FIGURE 36.4 The details page contains several features in addition to playing the selected recording.

The details page, shown in Figure 36.4 with its application bar expanded, enables playback, editing, and deletion of the selected sound.

The XAML for this page is shown in Listing 36.6, and the code-behind is shown in Listing 36.7.

LISTING 36.6 DetailsPage.xaml—The User Interface for Sound Recorder’s Details Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.DetailsPage”
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=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with 3 buttons and 2 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”pause”
IconUri=”/Shared/Images/appbar.pause.png” Click=”PlayPauseButton_Click”/>
<shell:ApplicationBarIconButton Text=”edit name”
IconUri=”/Shared/Images/appbar.edit.png” Click=”EditButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”reverse”
Click=”ReverseMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationTitle” Text=”SOUND RECORDER”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”{Binding ShortTitle}”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The playback slider –>
<TextBlock x:Name=”PlaybackDurationTextBlock” Grid.Row=”1”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,58,0,0”/>
<Slider x:Name=”PlaybackSlider” SmallChange=”.1” Grid.Row=”1”
Margin=”0,24,0,84”/>
<!– The playback speed slider with its reset button –>
<Grid Grid.Row=”2”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<TextBlock Text=”Playback Speed” Grid.ColumnSpan=”2” Margin=”12,0,0,0”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”SpeedSlider” Grid.Row=”1” SmallChange=”.1” LargeChange=”.1”
Minimum=”-1” Maximum=”1” Margin=”0,18,0,0”
ValueChanged=”SpeedSlider_ValueChanged”/>
<Button Grid.Row=”1” Grid.Column=”1” Content=”reset” Margin=”0,0,0,16”
VerticalAlignment=”Center” local:Tilt.IsEnabled=”True”
Click=”SpeedResetButton_Click”/>
</Grid>
<!– The “edit name” dialog –>
<local:Dialog x:Name=”EditDialog” Grid.RowSpan=”3” Closed=”EditDialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”Choose a name” Margin=”11,5,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox Text=”{Binding Result, Mode=TwoWay}” InputScope=”Text”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 36.7 DetailsPage.xaml.cs—The Code-Behind for Sound Recorder’s Details Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
Recording selectedRecording;
SoundEffectInstance soundInstance;
double durationScale = 1;
double elapsedSeconds;
DateTime lastPlayFrame;
SoundState lastSoundState = SoundState.Stopped;
IApplicationBarIconButton playPauseButton;
public DetailsPage()
{
InitializeComponent();
this.playPauseButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// The recording chosen from the list page
this.selectedRecording =
Settings.RecordingsList.Value[Settings.SelectedRecordingIndex.Value];
// The actual sound effect instance to play
this.soundInstance = this.selectedRecording.GetContent().CreateInstance();
// The page title data-binds to the ShortTitle property
this.DataContext = this.selectedRecording;
// Adjust the playback slider based on the recording’s length
PlaybackSlider.Maximum = this.selectedRecording.Duration.TotalSeconds;
// Start playing automatically
Play();
}
void Play()
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
this.playPauseButton.Text = “pause”;
this.playPauseButton.IconUri = new Uri(“/Shared/Images/appbar.pause.png”,
UriKind.Relative);
this.lastPlayFrame = DateTime.Now;
if (this.soundInstance.State == SoundState.Paused)
this.soundInstance.Resume();
else
{
// Play from the beginning
this.soundInstance.Play();
this.elapsedSeconds = 0;
}
}
void Pause()
{
this.playPauseButton.Text = “play”;
this.playPauseButton.IconUri = new Uri(“/Shared/Images/appbar.play.png”,
UriKind.Relative);
this.soundInstance.Pause();
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
if (this.soundInstance != null)
{
// Keep the playback slider up-to-date with the playing audio
if (this.soundInstance.State == SoundState.Playing ||
this.lastSoundState == SoundState.Playing
/* So remaining time after pausing/stopping is accounted for */)
{
this.elapsedSeconds +=
(DateTime.Now – lastPlayFrame).TotalSeconds / this.durationScale;
this.lastPlayFrame = DateTime.Now;
this.PlaybackSlider.Value = this.elapsedSeconds;
if (this.soundInstance.State == SoundState.Stopped)
this.PlaybackSlider.Value = this.PlaybackSlider.Maximum;
UpdatePlaybackLabel();
}
// Automatically turn the pause button back into a play button when the
// recording has finished playing
if (this.soundInstance.State != SoundState.Playing &&
this.playPauseButton.Text != “play”)
{
this.playPauseButton.Text = “play”;
this.playPauseButton.IconUri =
new Uri(“/Shared/Images/appbar.play.png”, UriKind.Relative);
// Unhook this event since it gets hooked on each play
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
this.lastSoundState = this.soundInstance.State;
// Required for XNA sound effect to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
}
void UpdatePlaybackLabel()
{
TimeSpan elapsedTime =
TimeSpan.FromSeconds(elapsedSeconds * this.durationScale);
TimeSpan scaledDuration =
TimeSpan.FromSeconds(PlaybackSlider.Maximum * this.durationScale);
PlaybackDurationTextBlock.Text = String.Format(“{0:00}:{1:00}”,
elapsedTime.Minutes, Math.Floor(elapsedTime.Seconds)) + “ / “ +
String.Format(“{0:00}:{1:00}”,
scaledDuration.Minutes, Math.Floor(scaledDuration.Seconds));
}
// Speed slider handlers
void SpeedSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
// Directly apply the -1 to 1 slider value as the pitch
this.soundInstance.Pitch = (float)SpeedSlider.Value;
// The duration scale used by other calculations ranges from
// .5 for double-speed/half-length (+1 pitch) to
// 2 for half-speed/double-length (-1 pitch)
this.durationScale = 1 + Math.Abs(this.soundInstance.Pitch);
if (this.soundInstance.Pitch > 0)
this.durationScale = 1 / this.durationScale;
UpdatePlaybackLabel();
}
void SpeedResetButton_Click(object sender, RoutedEventArgs e)
{
SpeedSlider.Value = 0;
}
// Handlers related to the “edit name” dialog
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (EditDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
EditDialog.Hide(MessageBoxResult.Cancel);
}
}
void EditDialog_Closed(object sender, MessageBoxResultEventArgs e)
{
this.ApplicationBar.IsVisible = true;
if (e.Result == MessageBoxResult.OK)
{
this.selectedRecording.Label = EditDialog.Result.ToString();
}
}
// Application bar handlers
void PlayPauseButton_Click(object sender, EventArgs e)
{
if (this.soundInstance.State == SoundState.Playing)
this.Pause();
else
this.Play();
}
void EditButton_Click(object sender, EventArgs e)
{
EditDialog.Result = this.selectedRecording.Label;
EditDialog.Show();
this.ApplicationBar.IsVisible = false;
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete this recording?”,
“Delete recording?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
// Remove it from the list
Settings.RecordingsList.Value.Remove(this.selectedRecording);
// Delete the audio file in isolated storage
this.selectedRecording.DeleteContent();
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void ReverseMenuItem_Click(object sender, EventArgs e)
{
this.selectedRecording.Reverse();
// We must get the new, reversed sound effect instance
this.soundInstance = this.selectedRecording.GetContent().CreateInstance();
// Re-apply the chosen pitch
this.soundInstance.Pitch = (float)SpeedSlider.Value;
Play();
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Sound Recorder”, UriKind.Relative));
}
}
}

[/code]

The Finished Product

Exit mobile version