Part 20: Recording an Audio Wav File
In this lesson we'll write the code required to record the custom sound. We'll employ the Coding4Fun Toolkit to help make this easier, but we'll still need to understand things like MemoryStreams and the phone's IsolatedStorage.
Here's our game plan in this lesson:
- We'll modify the ToggleButton wiring up event handler methods to the Checked and Unchecked events.
- We'll use the MicrophoneRecord class in the Coding4Fun Toolkit's Audio namespace to begin and stop the recording process.
- When we stop recording, we'll need to temporarily save the sound stored in the phone's memory to a location on disk so it can be played back or saved permanently.
- We'll add a MediaElement control so we can enable playback of the sound.
- Manage the state of the Play sound button, turning it off and on depending on the recording action of the user.
Let me just say that this is perhaps one of the most challenging lessons in this entire series because it deals with some slightly more advanced material. You should embrace this ... you only learn when you struggle, and challenging yourself with difficult concepts will help you grow faster. Be sure to not only watch this video, but also read the MSDN articles that I reference for more information. So put on your thinking cap and let's get started.
1. Modify the ToggleButton Control wiring up Event Handler Methods
Edit the XAML code on the RecordAudio.xaml page for the ToggleButton as follows:
In lines 39 and 40 we wire up method handlers for the two states of the ToggleButton.
2. Create a private instance of the Coding4Fun.Toolkit.Audio.MicrophoneRecord class
In the RecordAudio.xaml.cs file, add the following line of code:
We create a new private instance of the MicrophoneRecorder class, and use the hover-over-the-blue-dash technique to add a using statement for the Coding4Fun.Toolkit.Audio namespace.
Now, we can start and stop the MicrophoneRecorder by adding code to the ToggleButton's Checked and Unchecked event handler methods:
3. Saving the sound data collected by the MicrophoneRecorder into a file
As we're recording, the MicrophoneRecorder object is collecting the sound information in a buffer. A buffer is just a pocket of memory devoted to storing data. Buffers are typically used when there is a difference between the rate at which data is received and the rate at which it can be processed, or in the case that these rates are variable. So, it may take us 10 seconds to record a 10 second sound and during that time data is being added to the buffer. That said, the computer could process that data in a fraction of a second. The buffer is just a queue ... we can write the data at one rate of speed while reading it at another rate of speed. In programming, buffers are usually used between physical hardware and software, or when moving data from memory to disk and back, or data from memory to a network connection and back. I'm not a computer science guy, so I just think of a buffer as a bucket that you slowly collect things in until you're ready to work with the entire bucket at one time. So, I'm collecting shells on the beach and placing them into my bucket. Once I get a full bucket, I start processing them, deciding which to keep and which to throw away. I collect over a long period of time, then once I've filled my bucket, I process very quickly. That's how I think of a buffer.
When we call the Stop() method, the MicrophoneRecord stops adding sound information, but is holding that data in memory, in a buffer and NOW we need to process the data in the buffer. In this case, when I use the term "process" what I mean is that I want to grab the sound data out of the buffer and place it as a WAV file into a temporary file.
Once it's in a file sitting on my Phone's storage, I'll be able to hand that file over to a MediaElement and direct it to play that temporary WAV file. If the user wants to keep the file, I can re-name it and store it permanently.
So, let's talk about storing files on a Windows Phone. The storage space on the phone is partitioned into isolated areas. Each app installed on the phone gets one of these isolated areas. I use the term "isolated" because one app can't look at the storage area of another app. This is for security ... one app can't rummage through your photos or notes or other secret data and upload it to a secret malicious server or corrupt it in some evil way.
This isolated permanent storage area that's dedicated to your app is called IsolatedStorage. It's just a tidy way of keeping each app's data safe since each app is only able to write and read from its own storage area.
So, back to the problem at hand ... the MicrophoneRecorder object has a bunch on data sitting in a buffer in memory, a MemoryStream object. My job is to save that data from the MemoryStream to a file—a temporary file—in IsolatedStorage. To accomplish this, I'll create a helper method that will take a MemoryStream as an input parameter and then in the body of the helper method, I'll create a new file in IsolatedStorage and then dump all the MemoryStream buffer data into that file.
That's the plan, let's build it:
As the screenshot indicates, the input parameter is of type MemoryStream and will need a using statement to reference System.IO.
Next, we'll employ a different type of using statement:
In this context, the using statement will create a context for an instance of System.IO.IsolatedStorage.IsolatedStorageFile. Any code inside of the code block defined by the using statement will have access to the isoStore variable. Once the flow of execution leaves the closing curly brace, the isoStore variable will be disposed from memory properly.
You use this using syntax when you want to work with classes that implement IDisposable ... typically these are managed types in the .NET Base Class Library (or in our case, the Windows Phone API) that access UNMANAGED resources. So the primary use of the IDisposable interface is to release unmanaged resources. The garbage collector automatically releases the memory allocated to a MANAGED object when that object is no longer used. That said, it is not possible to predict when garbage collection will occur. Furthermore, the garbage collector has no knowledge of UNMANAGED resources such as window handles, or open files and streams. The danger is that two or more processes (apps) attempt to access the same resources at the same time and cause an unrecoverable error condition. Types implementing IDisposable handle these scenarios correctly. For a more complete explanation of these topics:
using Statement (C# Reference)
The IsolatedStorageFile class provides methods that help you manage the files and folder for use by your app. We use the GetUserStoreForApplication() to retrieve the IsolatedStorage area for just our app.
Next, we'll grab the buffer and attempt to create a file in our IsolatedStorage area:
In line 69, I add a line of code that will retrieve values from the buffer (passed in to the helper method) and convert it to the format of a wav (audio) file. As you can see, the MemoryBuffer doesn't implement this method. Instead, we want to use an extension method from the Coding4Fun.Toolkit.Audio.Helpers namespace. So, in order to apply this extension method, we'll add another using statement at the top of our code file:
An extension method in .NET allows you to attach a method to any type. So, I could add some utilities to the int or string or, in this case, the System.IO.MemoryStream. The extension method allows you to work with the members of that type just like any public method could. For more information on extension methods, and how to create your own, check out:
Note: this is an advanced topic ... as long as you understand what they do and what purpose they serve, that's enough for now. Later you can learn how to create your own.
Ok, so now we have the buffer in wav (audio) format, we just need to actually put it into a new file on the device's IsolatedStorage area:
(1) We create a temporary name for the wav file. We may discard this in the future (should the user hit the Record button again).
(2) The CreateFile() method creates a file on the file system using the name we pass in (line 73, above), and gives us back a reference to that we can use to write to.
(3) Now that we have an empty file to write to, we'll write the data stored in the bytes variable containing our sound. The Write() method's second and third parameters allows us to specify a portion of the sound data we want to write ... in this case, we'll write the entire file from the beginning (0) to the end (bytes.Length).
4. Add a MediaElement to play the new temporary file
In the RecordAudio.xaml file, beneath the ContentPanel grid control, we'll add a MediaElement control, give it a name, and set AutoPlay to false:
Back in the RecordAudio.xaml.cs, we'll programmatically set the source of our new MediaElement to our new temporarily sound file:
Now we're ready to play the sound file ... we just need to wire-up the event handler for the Play button.
5. Handle the Play button's Click Event to Test the Sound
In the RecordAudio.xaml file, in the definition for the Button element with the content "Play", we'll add a Click event handler called "PlayAudioClick":
Navigate to the new event handler and add the following line of code to call the Play() method of the MediaElement control:
At this point we have it all wired up, but it's not pretty ... it's pretty fragile ... so we'll need to comb back through this and program defensively. We'll do that later in this lesson.
For now, let's test our app. We've written a lot of code and, due to the nature of the functionality we've created, it wasn't possible to test it in small parts. We had to implement it all then test it to see where the problems pop up.
6. Declaring a Microphone capability for the app
Run the app, click the microphone icon in the application bar, then click the Record ToggleButton:
When you click Record one of two things probably happened ... either the app disappeared silently (no error message), or you'll get an exception, depending on which version of the Coding4Fun Toolkit you're using. The reason this doesn't work is because we do not have permission to use the phone device's microphone. To request permission to use the microphone, we need to declare a capability in our WMAppManifest.xml file.
In Windows Phone, you have to request permissions to use certain device capabilities. One purpose of this is to notify the user just how we intend to use their device. They may grow suspicious if our app wants to use their geolocation or their camera when that's not the purpose of our app.
The way to gain access to the capabilities of the phone's hardware is to open up the WMAppManifest.xml file and go to the second tab, "Capabilities":
- Open up the WMAppManifest.xml file
- Choose the second tab, Capabilities
- Add a checkmark next to the ID_CAP_MICROPHONE capability
If we re-run the app and go through the same set of steps, it should work. Note: to actually record sound in the Phone Emulator, you will need a working microphone hooked up to the computer. Make sure the mic works prior to starting this so that you can verify that it is not the source of any problems you might encounter:
I think the biggest issue at this point is that the code is pretty fragile and easily broken ... if we were to click the play button and immediately click the record button again, we could probably break the app, and (b) we don't have any visual feedback to let the user know what worked and what didn't, and (c) we are not actually saving the sound permanently, nor are we allowing the user to select a name for the new sound. We'll fix all of these in due time.
For now, let's focus on improving the quality of the code by adding defensive programming statements.
7. Add defensive programming statements to guard against potential exceptions
First, in the SaveTempAudio helper method, we'll make sure that there's actually data in our buffer before we attempt to create a file and fill it up with the buffer's contents:
Next, I want to guard against the possibility that the MediaElement is currently playing when the user taps the ToggleButton to record a sound. To do that, I'll refactor my code creating the IsolateStorageFileStream as a private member of the class:
Now, I'll add code to determine whether the _audioStream is empty. If it's not that means I have data that needs to be saved, then I need to empty it out. While I'm there, I'll make sure the MediaElement is no longer playing a sound, and its Source property is set to null. This will release any hold the MediaElement may have on the _audioStream from a previous try. It's possible that the MediaPlayer will keep a reference to the _audioStream from a previous recording, and I want it to release that reference before I try to create a new file using the same variable name _audioStream:
- Here I check to see whether _audioStream is null. If it's not, then I need to make sure that we're not in the middle of using the _audioStream in playback mode. In lines 74 and 75 I release the hold that the MediaElement has on the _audioStream, then ...
- I call dispose on the _audioStream. That should properly release any resources _audioStream is using. It's now ready to be re-populated with a newly created file handle.
- Here I re-work the code ... I don't need to create a new instance of _audioStream ... it's been cleaned out and all resources have been released. So, I can merely set _audioStream to a new file reference.
Now I want to think about how I create and reference the temporary file name. I want to make sure to not leave old temporary files around since they could clutter my apps IsolatedStorage area much like extra files take up too much space on our hard drives. The exception here is that your IsolatedStorage is much smaller than a hard drive, and audio files can be large. So, first I'll move it's declaration to a private member variable (line 24, below):
Next, I'll replace the:
var tempFileName = "tempWav.wav";
... with the following:
- If a temporary file already exists on the user's device from a previous recording attempt, we want to remove that file calling the .DeleteFile() method.
- We want to give the file a unique name and so we use the DateTime.Now.ToFileTime() to give it a unique name down to the second. That should be sufficient for our purposes.
Next, I want to disable the Play button when recording. That simple state management should make our lives as developers easier and reduce the possibility that the user is able to corrupt something. I'll give the Button the name "PlayAudio":
Next, I'll perform some state management in the ToggleButton's event handlers:
- When I start recording, I'll disable the PlayAudio button, and ...
- When I stop recording, I'll enable the PlayAudio button.
To recap, the big take aways from this lesson include utilizing the Coding4Fun Toolkit's Audio features to greatly reduce the complexity of using the Phone's microphone to record a sound. If you want to peek and see what it does, you can search for the full name of the class in Bing to find a code listing on Codeplex:
We learned about the other using statement in C# to properly dispose of managed classes that work with unmanaged resources. We learned about the function of buffers as a means of collecting data at one rate that could be processed at a different rate. We used the buffer built into the Coding4Fun MicrophoneRecorder class and retrieved the buffer to save it into a file on the device's storage. We learned about IsolatedStorage and how it protects each application's storage area keeping it private from the other apps on the system. We briefly learned about extension methods as we used one that was implemented in the Coding4Fun Audio Helpers namespace and, finally, we practiced some defensive programming techniques to ensure that our app can handle unique situations or edge cases.