Part 21: Permanently Saving the Audio Wav File
At this point, we are recording audio and saving it to a temp file on the app's IsolatedStorage. Next, we need to allow the user to permanently save the sound providing details like the display name for the new custom sound.
The game plan:
- Add an event handler method to the "save" application bar button
- We'll manage the state of the application bar ... it should only be visible if a temporary audio file has been created and is ready to be saved permanently
- We'll use the Coding4Fun Toolkit once again, this time to display an InputDialog to capture the name of the new custom sound audio file
- We'll serialize the data for the CustomSounds into a JSON file
- And we'll modify our data model to also load the CustomSounds JSON file to create new instances of the data model for those custom sounds
1. Add an event handler method to the "save" button and manage application bar state
Earlier we created the application bar for the RecordAudio.xaml page by enabling the BuildLocalizedApplicationBar() method. So all we need to do is make it active:
- In line 43 I add an event handler to the Click method using the technique we've utilized throughout this series ... while we're at it, use the other technique I've demonstrated to generate a method stub for the SaveRecordingClick method (hover-mouse-over-blue-dash to reveal an Intellisense menu option to generate the method stub)
- In line 46 I immediately hide the application bar ... we only want to show it when we have a sound to save (after the user records a custom sound)
Next, we'll enable the application bar after the user stops recording. In the RecordAudioUnchecked() method, we'll set the IsVisible property to true (see line 67, below):
2. Use the Coding4Fun Toolkit to display an InputDialog to capture the name of the new custom sound audio file
In the previous step we added a method stub for the SaveRecordingClick() method.
I'll replace the line of code that throws an exception as a reminder and write the following (line 51, below):
Since the InputPrompt is from a different namespace than the others we've utilized so far, we'll need to add a using statement (using the hover-over-the-blue-dash method to reveal the contextual menu).
Next we'll configure and show the InputPrompt:
- Here we set the Title and Message we want to appear in the InputPrompt
- We attach an event handler method (and generate the method stub using techniques I've demonstrated before) to the Completed event ... we'll tackle that in the next step
- Once configured, I show the dialog
When the user types in a name for the new custom sound and clicks the checkmark button, the FileNameCompleted() event handler method will fire.
We'll ensure that the InputPrompt was exited properly by the user by checking the result. We'll check the PopUpResult that was sent into this event handler method as an input parameter. If the result is "OK" then we can perform the logic necessary to save the temporary file as a new "permanent" sound. See the code I added below, as well as the code comments which give me an outline of the "next steps" I'll want to perform:
- If the user correctly typed in a new name and clicked the checkmark button to exit the InputDialog, then we'll perform the tasks required to save the custom sound and make it available in the Custom Sounds view of the Sound Board.
- Finally, we'll navigate back to the MainPage.xaml
Between callouts 1 and 2, above, are an outline of what needs to happen in order for this to work correctly. Before we attempt to implement those ideas, let's make sure the flow works as we would expect so far by running the application.
I record a custom sound by using the ToggleButton. When I stop recording, I in fact see the application bar appear:
When I click the disk icon to save the custom sound, it displays the InputDialog:
And when I type in a new sound name and click the checkmark icon, the dialog disappears and returns me to the MainPage.xaml. Great!
Now, for the hard part ... we'll perform those tasks I outlined in the code comments.
3. Save the sound file into a permanent IsolatedStorage area, serialize the data for the CustomSounds into a JSON file
At this point we have a custom sound recorded and stored as a temporary file and we've just collected a friendly display name for that sound. We want to accomplish two basic tasks:
- First, we want to add the custom sound to our data model. If information about our new custom sound is never added to the data model, then we'll never be able to render it to the Custom Sounds view on our MainPage.xaml. So, we'll create a new instance of the SoundData class and fill in the FilePath and Title properties appropriately.
- Next, we'll want to move that file from its temporary location to a permanent subfolder called /customAudio/ ... this is purely to keep all our custom sound files organized in one place.
So, I add the following code to the FileNameCompleted() method:
- I create a new instance of SoundData and fill in the Title and FilePath attributes. Notice that we'll be giving our custom sound a new name, but the contents of the file will remain the same.
- Just like when we originally recorded the custom sound, we get a reference to the IsolatedStorage area specifically for our app. We do this with a using statement to properly let go of unmanaged resources (like the Phone's storage). The very first time this code is executed, it may need to create the special folder where we're storing our custom audio files (line 76). Finally, we're moving the temporary file to the new permanent storage area and giving it a new name all in one fell swoop (line 78).
- Next, we're adding the new instance of the SoundData class to our CustomSounds.Items collection. At this moment, we should be able to return back to the MainPage.xaml and see the new custom sound appear in the list of Custom Sounds.
However, what will happen when we close the application and it is complete removed from the Phone's memory? When that happens, the CustomSounds.Items collection will be removed from memory and the next time the app is run, it will have no memory of our custom sounds. We need a way to store our custom sounds data so that we can load it into our data model the next time the user runs our app.
4. Serialize and deserialize the CustomSounds SoundGroup into / out of Json
To begin, we'll open up the NuGet Package Manager (using the technique I demonstrated earlier ... right-click the References folder and choose the Manage NuGet Packages ... option.
- Search for: Json ... one of the top options should be Json.NET.
- Click the Install button next to the Json.NET package. It will take a few moments to install that package into your project.
- Click the Close button to continue.
To verify that Json.NET was installed successfully, open up the References folder in the SoundBoard project and verify that Newtonsoft.Json appears there:
Back in the FileNameCompleted() method, the next step is to convert the CustomSounds.Items collection to Json, then store it to disk.
We'll use the Newtonsoft.Json.JsonConvert class to perform the conversion ... you'll need to add the appropriate using statements to accommodate the JsonConvert class:
Now we're ready to fully implement the storage of the CustomSounds Json file to disk.
- We use the JsonConvert.SerializeObject() method to serialize the CustomSounds object (and all it's children, etc.) into Json
- We'll use a special area of IsolatedStorage called IsolatedStorageSettings to save this object data. This is a simple way to save settings for your app. You can use the name / value pair pattern to save any settings you like for your app. So, in our case, we'll create a key and supply the value ... the value part is obviously the data -- the serialized Json data from the previous line of code. The key part will be a literal string that we'll create as a constant property in the SoundModel class definition. We'll need that key later to RETRIEVE the Json data back out of the IsolatedStorageSettings (we'll do that later in this lesson).
- We'll call the Save() method to actually save our new ApplicationSetting, the new name / value pair we created in the previous line of code.
Before we forget, let's implement that CustomSoundKey ... in the SoundModel.cs file, I'll add the following line of code (line 19, below):
As you can see, this is nothing more than a constant string value. We want it to be constant because it should never change. It's simply a unique string we'll use as the means of getting back at the right ApplicationSetting in the IsolatedStorageSettings store.
Next, we'll want to load our custom sounds into memory at the same time we instantiate all of the other SoundGroup objects ... in the SoundModel.cs file, in the LoadData() method:
In line 28 (above) we'll call a helper method, LoadCustomSounds(), to populate the CustomSounds property of the SoundModel class. Use the technique I demonstrated earlier to generate a method stub for this new method.
In our new LoadCustomSounds() method, we'll attempt to retrieve the Json that contains the serialized Custom Sound data from the IsolatedStorageSettings:
- We perform a TryGetValue() method on the IsolatedStorageSettings.ApplicationSettings ... if the CustomSoundKey exists in IsolatedStorage, then it should return the value (i.e., the Json we stored previously) into the out parameter, "dataFromAppSettings". If not, then the else code block will create a new (empty) instance of SoundGroup.
- Now that we have the serialized data, we want to DESERIALIZE that data back into instances of SoundGroup (and SoundData) objects. We call the generic DeserializeObject<T>() method, giving it the type we expect to deserialize it into (i.e., SoundGroup) and pass in the data we retrieved from the IsolatedStorageSettings.
- Assuming the TryGetValue failed, that means no custom sounds were created (or perhaps there was a problem retrieving the data). In either case, return an empty SoundGroup.
Now we cross our fingers and test the app. I'll record a sound and attempt to save the sound with the name "another test".
All looks good until I return to the MainPage.xaml after I've saved the new custom sound and attempt to play it. However, it doesn't play back! That's because we need to modify the playback code on the MainPage.xaml to load CustomSounds FROM THE NEW FOLDER! It only loads from the /Assets folder right now!
The goal is to set the MediaElement's Source property with the correct location of the sound file associated with the tile our user tapped. We'll look in one of two places ... either in the /Assets folder, or in the IsolatedStorage area.
In the MainPage.xaml.cs, in the LongListSelector_SelectionChanged() event handler method I add the following:
Here I'm attempting to see if the tile selected by the user is a custom sound. If we can't locate the file name associated with the tile in the Assets\ folder, then we'll look for it in the /customSounds/ sub-folder where in the app's IsolatedStorage area.
For this check, we'll need to access the file system of the Phone, so we'll use the System.IO.File object. The screenshot above shows how we will want to add the appropriate using statement at the top of the code file to include System.IO.
Additionally, we'll want to include a reference to System.IO.IsolatedStorage since we'll be working with classes in that namespace next:
Back in the LongListSelector_SelectionChanged() event handler method, the File.Exists() will return false if the SoundData object's FilePath cannot be found in the default location. Otherwise it will be in the /Assets folder. So, based on that I write the following code:
- Here I am setting the MediaElement's Sound property like before using the SoundData's FilePath property
- In this case, the file was not in the /Assets folder so we'll search for it in IsolatedStorage. Here we create a reference to the IsolatedStorage for our app. Note the using statement so that we can properly dispose of this resource when we're finished.
- Here we get access to the file using a new technique. We're opening the custom sound as a stream, or more specifically, a IsolatedStorageFileStream. More about streams in a moment.
- Here we use the SetSource property of the MediaElement to the stream containing our custom sound from IsolatedStorage.
This time when we run the app, record and save a sound, then return to the "mine" (Custom Sounds) category, each of our saved custom sounds should play correctly!
To recap, the big take aways from this lesson were Serializing and Deserializing objects into JSON using Newtonsoft Json, which is a very valuable skill that transcends Phone development. We also learned how to work with IsolatedStorage, particularly the IsolatedStorageSettings to store name / value pairs. We used the System.IO.File class to examine the file system, and learned about working with streams. We also used the InputPrompt from the Coding4Fun Toolkit and a bunch more. We are almost done ... we'll just add one last touch to the app to make it fun to use.