Now that we've designed the model, let's go ahead and implement it.
Like in our previous projects, we'll have two data models: the first one to deal with the collection of playable files, and the second one to deal with handling a specific playable file. The first, VoiceRecDocumentCollection.js
is quite similar to our previous projects, and so we won't go over it in this task. But the VoiceRecDocument.js
file is very different, so go ahead and open it up (it's in the www/models
directory), so you can follow along.
Let's start using the following code snippet:
DOC.VoiceRecDocument = function(theFileEntry, completion, failure) { var self = this; self.fileEntry = theFileEntry; self.fileName = self.fileEntry.fullPath; self.fileType = PKUTIL.FILE.getFileExtensionPart(self.fileName); self.completion = completion; self.failure = failure; self.state = "";
As in our prior projects, the incoming parameters include a file entry obtained from the File
API. We'll use this to determine the name of the audio file to play as well as its type. We're using some new functions, introduced in this version of the framework, to do work with the various portions that make up a file, namely, the path, the filename, and the file extension. Earlier, we use PKUTIL.FILE.getFileExtensionPart()
to obtain the type of the file, whether it is an MP3, WAV, or something else.
self.title = PKUTIL.FILE.getFileNamePart(self.fileName); self.media = null; self.position = 0; self.duration = 0; self.playing = false; self.recording = false; self.paused = false; self.positionTimer = -1; self.durationTimer = -1;
Here we define several properties that we will use to keep track of the various states and timers we need to use to properly manage our audio:
title
: This property gives the title of the file, essentially the filename minus the path and extension.media
: This property gives the Media
object provided by PhoneGap. This property will be set whenever the program needs to play a sound or record something.position
: This property gives the approximate position in the sound file for playback. It's approximate because it is updated every few milliseconds with the position. We'll discuss why in a bit.duration
: This property gives the duration of the sound file (if playing), or the approximate duration of the recording (while, or after, recording).playing
, recording
, paused
: These are simply Boolean variables intended to make it easy to determine what we're doing. Are we playing the file, recording a file, and, if we're playing, are we paused?durationTimer
, positionTimer
: Timer IDs are used to track the intervals that get created whenever we load a media file or prepare one for recording. The durationTimer
property updates the duration
property, and the positionTimer
property updates the position.self.getFileName = function() { return self.fileName; } self.setFileName = function(theFileName) { self.theFileName = theFileName; self.fileType = PKUTIL.FILE.getFileExtensionPart(self.fileName); self.title = PKUTIL.FILE.getFileNamePart(self.fileName); }
The preceding code snippet handles getting and setting the filename. If we set a filename, we have to update the filename, type, and title.
self.initializeMediaObject = function() {
This method is a very important method; we'll be calling it at the top of most of our methods that work with playback or recording. This is to ensure that the media
property is properly initialized. But it is also to ensure a few other details are correctly set up, as follows:
if (self.media == null) { if (PKDEVICE.platform()=="android") { self.fileName = self.fileName.replace ("file://",""); }
First, we do these steps if and only if we don't already have a media
object at hand. If we do, there's no need to initialize it again.
Secondly, we check if we're running on Android. If we are, the file://
prefix that comes out of the File
API will confuse the Media
APIs, and so we remove it.
self.media = new Media(self.fileName, self.dispatchSuccess, self.dispatchFailure);
Next, we initialize the media
property with a new Media
object. This object requires the filename of the audio file, and two functions: one for when various audio functions complete successfully (generally only when playback or recording has stopped), and another for when something goes wrong.
self.positionTimer = setInterval(self.updatePosition, 250); self.durationTimer = setInterval(self.updateDuration, 250); } }
Finally, we also set up our two timers to update every quarter of a second. These times could be made faster or slower depending upon the granularity of updates you like, but 250
milliseconds seems to be enough.
self.isPlaying = function() { return self.playing; } self.isRecording = function() { return self.recording; }
Of course, like any good model, we need to provide methods to indicate our state. Hence, isPlaying
and isRecording
are used in the preceding code snippet.
self.updatePosition = function() { if (self.playing) { self.media.getCurrentPosition(function(position) { self.position = position; }, self.dispatchFailure); } else { if (self.recording) { self.position += 0.25; } else { self.position = 0; } } }
If you recall, this function is called continuously during playback and recording. If playing, we ask the Media
API what the current position is, but we have to supply a callback method in order to actually find out what the position is. This should usually be called nearly instantly, but we can't guarantee it, so this is why we have encapsulated obtaining the position somewhat. We'll define a getPosition()
method later that just looks at the position
property instead of having to do the callback every time we want to know where we are in the audio file.
self.updateDuration = function() { if (self.media.getDuration() > -1) { self.duration = self.media.getDuration(); clearInterval(self.durationTimer); self.durationTimer = -1; } else { self.duration--; if (self.duration < -20) { self.duration = -1; clearInterval(self.durationTimer); self.durationTimer = -1; } } }
Obtaining the duration is even harder than obtaining the current position, mainly because it is quite possible that the Media
API is streaming a file from the Internet rather than playing a local file. Therefore, the duration may take some time to obtain.
For as long as the duration timer is running, we'll ask the Media
API if it has a duration for the file yet. If it doesn't, it'll return -1
. If it does return a value greater than -1
, we can stop the timer, since once we get a duration, it isn't likely to change.
There's no need to keep asking for the duration forever, especially if we can't determine the duration, so we use the negative numbers -1
to -20
of our duration
property as a kind of timeout. We subtract one each time we fail to obtain a valid duration, and if we go below -20
, we give up by stopping the timer.
self.getPlaybackPosition = function() { return self.position; } self.setPlaybackPosition = function(newPosition) { self.position = newPosition; self.initializeMediaObject(); self.media.seekTo(newPosition * 1000); }
Getting the playback position is now simple, we just return our own position
property. But sometimes we need to change the current playback position. To do this, we use the seekTo()
method of the Media
API to adjust the position. For whatever reason, the position used in the seekTo()
method is in milliseconds, while the position we obtain constantly with our timer is in seconds, hence the multiplication by 1000
.
self.getDuration = function() { return self.duration; } self.startPlayback = function() { self.initializeMediaObject(); self.media.play(); self.paused = false; self.recording = false; self.playing = true; } self.pausePlayback = function() { self.initializeMediaObject(); self.media.pause(); self.playing = false; self.paused = true; self.recording = false; }
Starting playback is actually very simple: once we initialize the object, we just call the play()
method on it. Playback will start as soon as possible. We also set our state properties to indicate that we are playing.
Once playing, we can also pause easily: we just have to call the pause()
method. We update our state to reflect that we are paused as well.
self.releaseResources = function() { if (self.recording) { self.stopRecording(); } if (self.positionTimer > -1) { clearInterval(self.positionTimer); } if (self.durationTimer > -1) { clearInterval(self.durationTimer); } self.durationTimer = -1; self.positionTimer = -1; self.media.release(); self.media = null; }
Since media files can consume a lot of memory, whenever they aren't in use, they should be released from memory. When we release the file, we also need to stop the timers, if running).
self.stopPlayback = function() { self.initializeMediaObject(); self.media.stop(); self.isPlaying = false; self.isPaused = false; self.isRecording = false; }
Stopping playback is quite simple: just call the stop()
method instead of the pause() method
. The difference between the two is that pausing playback allows a subsequent call to the play() method
to resume immediately where we paused. Calling the stop()
method will reset our position to zero, so the next play()
method will start from the beginning.
self.startRecording = function() { self.initializeMediaObject(); self.media.startRecord(); self.isPlaying = false; self.isPaused = false; self.isRecording = true; } self.stopRecording = function() { self.initializeMediaObject(); self.media.stopRecord(); self.isPlaying = false; self.isPaused = false; self.isRecording = false; }
Recording is similarly easy: we just call startRecord()
or stopRecord()
. There is no functionality for providing support for pausing in the middle of recording.
self.dispatchFailure = function(e) { console.log("While " + self.State + ", encountered error: " + e.target.error.code); if (self.failure) { self.failure(e); } }
Our failure
method is pretty simple. If an error occurs, we'll log it, and then call the failure
method given when creating this object.
self.dispatchSuccess = function() { if (self.completion) { self.completion(); } } }
The success
function is even simpler: we just call the completion()
method passed in when creating the object.
In this task, we created the data model for a specific audio file as well as the methods for initiating, pausing, and stopping playback, and those for initiating and stopping recording.
The completion
method is generally called at the end of playback, though it can be called for other reasons as well. In general, one would use this to clean up the media object, but if it is called when not expected, the result would be an abrupt stop of playback.
The other important issue is that each platform supports only certain media files for playback and even different ones for recording. Here's a short list:
Platform |
Plays |
Records |
---|---|---|
iOS |
WAV |
WAV |
Android |
MP3,WAV, 3GR |
3GR |
18.226.165.131