Today's demo is something I started working on Sunday "for fun", but when it turned into an unholy mess (see Recording and saving audio in Cordova applications), it took me a bit longer to wrap than I expected. The idea was simple. "Sound Boards" are apps that contain a collection of sounds, typically related to a movie or TV show. My coworker Andy built a cool sound board themed around Halloween a few years back: Halloween Fun with PhoneGap. I wanted to build a sound board too, but instead of shipping it with a set of sounds, I wanted it to be completely customized. The idea being you could record your own sounds. In a fit of extreme creativity, I called it - "My Sound Board".
Let me share some of the screens behind the app and then I'll dive into the code. On launch, the app will present you with a list of sounds you currently have or prompt you to record a new one.

Clicking record brings you to a new UI:

On this screen you can record a sound, play it back to test, and name it. The recording interface will be device specific on Android, but on iOS it is a standard UI created by the Media Capture plugin itself. Here it is on my HTC M8.

And here it is on an iPhone.

Once you've saved a few sounds, you can see the list grow.

I then used a cool Ionic feature that makes it easy to add delete buttons to a list. If you swipe right, you get a button:

All I had to do then was wire up the logic to handle deleting. So - about that code - let's take a look. I won't cover the workaround I mentioned in this weeks blog post, but I strongly suggest reading it. I'll begin with the controller code for the home page:
.controller('HomeCtrl', function($scope, Sounds, $ionicPlatform) {
var getSounds = function() {
console.log('getSounds called');
Sounds.get().then(function(sounds) {
console.dir(sounds);
$scope.sounds = sounds;
});
}
$scope.$on('$ionicView.enter', function(){
console.log('enter');
getSounds();
});
$scope.play = function(x) {
console.log('play', x);
Sounds.play(x);
}
$scope.delete = function(x) {
console.log('delete', x);
Sounds.get().then(function(sounds) {
var toDie = sounds[x];
window.resolveLocalFileSystemURL(toDie.file, function(fe) {
fe.remove(function() {
Sounds.delete(x).then(function() {
getSounds();
});
}, function(err) {
console.log("err cleaning up file", err);
});
});
});
}
$scope.cordova = {loaded:false};
$ionicPlatform.ready(function() {
$scope.$apply(function() {
$scope.cordova.loaded = true;
});
});
})
There's nothing really interesting here except for the $ionicView.enter handler. That's how I handle getting my sounds every time you hit the home page. Technically, that is a bit wasteful. I should only care when I modify my sounds. But this was simpler, and as it stands, if you aren't recording and leaving the home page, then there is no real performance issue. Note that delete is a bit complete. I decided that my Sound service would not worry about files. My RecordCtrl for example handles the file copying that I mentioned in the earlier post. So when I delete, I have to kill the file myself, and then tell the service.
Ok, let's look at the view.
<ion-view view-title="My Sound Board">
<ion-content class="padding">
<div ng-if="sounds.length > 0">
<ion-list show-delete="false" can-swipe="true">
<ion-item ng-repeat="sound in sounds" ng-click="play($index)">
{{sound.name}}
<ion-option-button class="button-assertive" ng-click="delete($index)">
Delete
</ion-option-button>
</ion-item>
</ion-list>
</div>
<div ng-if="sounds.length < 1">
<p>
No sounds yet. Why not record a new one?
</p>
</div>
<a href="#/new" class="button button-balanced button-block" ng-show="cordova.loaded">
Record New Sound
</a>
</ion-content>
</ion-view>
Right away, I can say I don't like the two divs there. I have to think Angular has a way of doing an ELSE type condition. Can anyone suggest an improvement? I absolutely love how Ionic adds the "swipe to show button" logic. I literally just used can-swipe and then defined a button.
Now let's look at the service.
angular.module('mysoundboard.services', [])
.factory('Sounds', function($q) {
var deleteSound = function(x) {
console.log("calling deleteSound");
var deferred = $q.defer();
getSounds().then(function(sounds) {
sounds.splice(x,1);
localStorage.mysoundboard = JSON.stringify(sounds);
deferred.resolve();
});
return deferred.promise;
}
var getSounds = function() {
var deferred = $q.defer();
var sounds = [];
if(localStorage.mysoundboard) {
sounds = JSON.parse(localStorage.mysoundboard);
}
deferred.resolve(sounds);
return deferred.promise;
}
var playSound = function(x) {
getSounds().then(function(sounds) {
var sound = sounds[x];
/*
Ok, so on Android, we just work.
On iOS, we need to rewrite to ../Library/NoCloud/FILE'
*/
var mediaUrl = sound.file;
if(device.platform.indexOf("iOS") >= 0) {
mediaUrl = "../Library/NoCloud/" + mediaUrl.split("/").pop();
}
var media = new Media(mediaUrl, function(e) {
media.release();
}, function(err) {
console.log("media err", err);
});
media.play();
});
}
var saveSound = function(s) {
console.log("calling saveSound");
var deferred = $q.defer();
getSounds().then(function(sounds) {
sounds.push(s);
localStorage.mysoundboard = JSON.stringify(sounds);
deferred.resolve();
});
return deferred.promise;
}
return {
get:getSounds,
save:saveSound,
delete:deleteSound,
play:playSound
};
});
I decided to use LocalStorage for my persistence. Normally I recommend against LocalStorage when working with user generated content like this. But I decided that since the user data would be fairly small (a short array), then it was safe. The ease of use of LocalStorage is what sealed the deal. I felt kind of bad using outside stuff like LocalStorage and the Media plugin, but I got over it.
And that's pretty much it. Here is a video with some sounds I recorded of my kids. The whole reason I even thought of this app was that one of my boys wanted to record his younger sisters being silly, so naturally I made use of them.
Finally, you can find the complete source code for this application here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/mysoundboard. I need to add a readme to the folder, and I promise I'll get to that. Eventually.
Archived Comments
Why is it necessary on iOS to "rewrite to ../Library/NoCloud/FILE" ?
I have a problem where file is present but does not play.
I use download plugin to download to window.cordova.file.documentsDirectory, and it works. Media does not play the file. I am lost as to why.
Why is it necessary? I don't know - that's just how it worked for me. Are you saying my code isn't working?
Oh, no, the opposite it true. Your code is the only thing that works on iOS - and that is killing me. The full path, starting with file:// and all the way down to the file works for the download plugin, for file presence checking plugin but not for media plugin. That is crazy. So 1. you saved my day and 2. the plugin is ( or was, just to be fair, I use 1 year old plugin as it is available on PhoneGap Build, I don't have mac to create my own build system) problematic. So thank you very very much, dear Sir.
Glad to be helpful. :)
hi, I was Copy paste all your code from github but i'm found an error (see picture) sorry if my question so easily to fix but I'm newbie so I don't know what should I do to fix that error , thank you so much if you want to help me :)
I responded on the other post.
Get what error?
And as an FYI, Record will *not* work in the Chrome browser. You need to test on simulator or device.
Please help as I am gettoing the same error & I am not able to see your other post
I believe Rehatul had posted the same q on two posts, hence my comment, but I don't remember the issue. You can try removing the scope apply part.
Hi Raymond. Thanks for the insightful blog and the code. I seem to be having a problem running the code. I put the code as is in an ionic blank project, built it for iOS and ran it on a real device (iPhone 6).
When I click on the Record button, i get "navigator.device.capture" is undefined. Can you please tell me what am I missing here.
Thanks a lot in advance.
-NK
You forgot to add the plugin.
Hey Raymond,
I am trying to get the code running aswell but can not find the correct plugin for the localStorage functionality. Could you point me towards the precise name so that I can run it?
Thanks!
There is no localStorage plugin - it is built into the webview. For capture, you want cordova-plugin-media-capture.
Hey Raymond, thanks for the reply!
I have already added the media-capture plugin and it correctly goes from the app to the microphone. The problem I am having is that once I click save in the microphone app, I still can't see any sound recordings. Any ideas where things might be going wrong for me or ideas on how to find out?
Thanks!
If you remote debug w/ Chrome or Safari, do you see any errors in the console?
Hi Raymond,
is possible save audio in ios 10 in the mp3 or m4a directly from the component? or I try to use a plugin for compressed file? thank's
Best regards Alessandro
20 days late - sorry. Afaik you don't have control over the way the sound file is saved.
Hi Raymond,
Recorder not working in marshmallow. please help. recorder not open. other android phone is working.
Do you see anything when you remote debug?
It's working on marshmallow 6.0. but not working on Samsung 7 default
S voice recording.
See my comment - what, if anything, did you see when you remote debugged?
Hola gracias por compartir
Puedes explicarlo como se hace en la versión ultima de Ionic 2. Gracias
No hablo espanol.
Great stuff! This is exactly what I need now. But I am a real newbie here, and I downloaded the complete code, but I am not sure how to run it on my PC to test it on iPhone. there is still no readme file in the code package yet. Please help.
If you are _very_ new to Ionic and Cordova, you should start w/ Cordova first - cordova.apache.org. Look at the getting started guide there. If you go to my About page, I've got a link to a book I wrote on Cordova, but that will cost you money. :)
Hi,
Why i'm Getting a "Media it s not define error?"
$scope.play = function() {
if(!$scope.sound.file) {
navigator.notification.alert("Record a sound first.", null, "Error");
return;
}
var media = new Media($scope.sound.file, function(e) {
media.release();
}, function(err) {
console.log("media err", err);
});
Did you install the plugin?
Hi,
I am using your recording code , and tried to run but i am not able to play the recorded sound.Rest Everything is working but not able to get any error code on play event. I have tried on Galaxy s3 samsung and motorola gen1 android phone . One was taking sd card while motorola was saving in internal storage.Please suggest what i am missing.
Thank you
Do you see anything in Chrome's console when you remote debug?
Hi,
Code works Fine
very helpful
Thank you