Sound Recorder (Saving Audio Files & Playing Sound Backward)

1
169

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.

The three possible states of the main page.
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]

  • The page is portrait-only due to the dimensions of the artwork and the exact layout of the controls surrounding it.
  • The second image, OffAirImage, is shown during the stopped and paused states. It covers the illuminated “on air” sign with one that is off, and it also dims the VU meter (whose needle still moves in response to sound). It accomplishes the dimming with a translucent region in the image, as demonstrated in Figure 36.2.
  • The four buttons are instances of a simple user control called ImageButton that is included with this app’s source code. Rather than doing tricks with opacity masks to get the inverted-colors-when-pressed effect, this control simply asks for two separate image files. It displays PressedSource when pressed; otherwise it displays Source.
  • Because background.png (with fixed colors) fills the page, the text and images in this page all use a hard-coded white color.
  • The VU meter needle is implemented as a Line element whose RotateTransform is manipulated from code behind. Its RenderTransformOrigin of .5,1 rotates it around its bottom edge. It is positioned with Canvas.Left and Canvas.Top rather than solely with its X1 and Y1 properties—and it is given an explicit width and height—to make the transform work more understandably.
The overlay image replaces the photo and dims the sound meter.
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]

  • The structure of this code is pretty similar to the preceding two chapters. In this app, the microphone is started from the constructor to enable the VU meter needle to move at all times, not just while recording is in progress. During development, ensure that your application manifest contains the ID_CAP_MICROPHONE capability, otherwise the call to Microsoft.Default.Start will fail due to Microphone.Default being null.
  • In the microphone’s BufferReady event handler, the buffer is written to the stream, but only if we’re recording. (The three states of the page are indicated by the threevalue AudioState enumeration.) The average volume of the sample is used to determine where the needle should be placed, transforming the value to a range from MIN_ANGLE (-55°) to MAX_ANGLE (55°). The GetAverageVolume method is identical to the one from the preceding two chapters.
  • CompositionTarget_Rendering not only performs the requisite call to FrameworkDispatcher.Update, but it also takes the opportunity to adjust the needle based on the value of targetNeedleAngle calculated in Microphone_BufferReady. If the difference between the current angle and the target angle is small enough, the needle is directly moved to the target angle. Otherwise, the needle is moved a fraction of the necessary distance each time CompositionTarget_Rendering is called to provide a smooth animation. The speed of “downward” motion (decreasing the angle due to softer audio) is limited to DOWNWARD_VELOCITY to provide a more realistic effect of a needle that can jump to a louder volume but always smoothly returns to its resting position. This isn’t meant to be an accurate simulation of VU meter ballistics, but it should look good enough to most people.
  • CurrentState’s property setter updates the user interface the old-fashioned way; by touching several properties on several elements. It would look more satisfactory if the relevant elements were data-bound to a version of CurrentState that is either a dependency property or a property that manually raises change notifications. However, several value converters would be needed to morph the enumeration value into the variety of Visibility values and strings needed. The end result would involve much more code.
  • The code that actually saves the audio data is at the end of the listing in StopButton_Click. To do this, the memory stream holding the data is passed to SaveContent on a custom Recording class shown in the next listing.
  • This app uses two persisted settings, defined as follows in a separate Settings.cs file:

    [code]

    public static class Settings
    {
    // The user’s data
    public static readonly Setting<ObservableCollection<Recording>>
    RecordingsList =
    new Setting<ObservableCollection<Recording>>(“RecordingsList”,
    new ObservableCollection<Recording>());
    // Communicate the selection on the list page to the details page
    public static readonly Setting<int> SelectedRecordingIndex =
    new Setting<int>(“SelectedRecordingIndex”, -1);
    }
    [/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]

  • Any changes to Label, TimeStamp, or Duration also raise property-changed notifications for Title, Subtitle, and ShortTitle, three readonly properties whose value is based on these read/write properties. The list page leverages Title and Subtitle in the display for each recording, and the details page leverages ShortTitle.
  • The implementation of SaveContent is a straightforward writing of the passed-in stream’s bytes to the isolated storage file indicated by the Filename property. This method also automatically sets Duration to the length of the recording (revealed by the microphone) so this information can be leveraged in other parts of the app without having to load the audio file. This is especially important for the list page, which displays the duration for every recording simultaneously.
  • When GetBuffer reads in the data from isolated storage, it knows how big the buffer needs to be ahead of time, thanks to the stored duration (and thanks to the microphone’s GetSampleSizeInBytes method). GetBuffer is not public; consumers instead call GetContent, which returns the audio data as a familiar SoundEffect object.
  • DeleteContent deletes the file containing the audio data, just like the same-named method from the Notepad app.
  • The Reverse method does the trick of reversing the audio file. It’s simply a matter of reversing the bytes, except that this needs to be done in 2-byte chunks to keep each 16-bit audio sample intact.

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.

The CheckableListBox supports multi-select interaction the same way as the phone’s built-in Mail app.
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 checkable list box data-binds to the RecordingsList setting and stays up-todate thanks to the observable collection and property-changed notifications from each item. Its SelectionMode property, inherited from the base list box control, is set to Multiple to enable bulk-selection to work.
  • Tapping an item navigates to the details page filled out for that item. This is communicated via the SelectedRecordingIndex setting that is set appropriately before navigating.
  • When an item is deleted, the code makes sure to call DeleteContent in addition to removing the Recording instance from the list. Otherwise, the audio file would be left behind in isolated storage.
  • The rest of the code manages the CheckableListBox. Bulk-selection mode (just called select mode in the listing) can be triggered either by tapping an item’s left margin (handled internally in code for CheckableListBoxItem) or by tapping the “select” button on the application bar. As in the Mail app, bulk-selection mode can be exited either by pressing the hardware Back button or by unchecking all of the check boxes.

The Details Page

The details page contains several features in addition to playing the selected recording.
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]

  • All three rows in this page’s root grid are given a height of Auto, so the content doesn’t shift when the application bar is hidden during the display of the “edit name” dialog.
  • The Dialog user control used by several apps in this book is leveraged here to enable the user to rename the recording. This name is applied as the Label property on the Recording instance, which impacts the Title, ShortTitle, and Subtitle properties as shown in Listing 36.3.
  • The CompositionTarget_Rendering handler, used only while the audio is playing, keeps the slider and pause/play button in sync with the audio.
  • The speed slider uses the familiar Pitch property on SoundEffectInstance to adjust the audio as it plays. This affects speed and pitch, as there’s unfortunately no builtin way to adjust the speed while maintaining the pitch.
  • The audio reversal is done inside ReverseMenuItem_Click. Because the reversal is done to the file in isolated storage, the sound effect instance must be retrieved again. Invoking the reversal a second time restores the audio file to its original data.

The Finished Product

Sound Recorder (Saving Audio Files & Playing Sound Backward)

1 COMMENT

Comments are closed.