Cordova/Ionic Sample App: My Sound Board

This post is more than 2 years old.

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.

shot1

Clicking record brings you to a new UI:

shot2

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.

shot3

And here it is on an iPhone.

shotThu2

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

shot4

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:

shot5

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.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Michael Kariv posted on 9/22/2015 at 3:13 PM

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.

Comment 2 (In reply to #1) by Raymond Camden posted on 9/23/2015 at 1:07 PM

Why is it necessary? I don't know - that's just how it worked for me. Are you saying my code isn't working?

Comment 3 (In reply to #2) by Michael Kariv posted on 9/23/2015 at 2:21 PM

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.

Comment 4 (In reply to #3) by Raymond Camden posted on 9/24/2015 at 3:29 PM

Glad to be helpful. :)

Comment 5 by Rehatul Ambar posted on 1/6/2016 at 4:48 AM

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 :)

Comment 6 (In reply to #5) by Raymond Camden posted on 1/6/2016 at 2:26 PM

I responded on the other post.

Comment 7 (In reply to #0) by Raymond Camden posted on 4/5/2016 at 3:52 PM

Get what error?

Comment 8 (In reply to #7) by Raymond Camden posted on 4/5/2016 at 3:52 PM

And as an FYI, Record will *not* work in the Chrome browser. You need to test on simulator or device.

Comment 9 (In reply to #6) by Ankit posted on 4/30/2016 at 6:44 PM

Please help as I am gettoing the same error & I am not able to see your other post

Comment 10 (In reply to #9) by Raymond Camden posted on 4/30/2016 at 9:13 PM

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.

Comment 11 by Kishore Nanda posted on 6/2/2016 at 10:22 AM

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

Comment 12 (In reply to #11) by Raymond Camden posted on 6/2/2016 at 12:49 PM

You forgot to add the plugin.

Comment 13 (In reply to #12) by Maikel Grobbe posted on 6/5/2016 at 8:25 PM

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!

Comment 14 (In reply to #13) by Raymond Camden posted on 6/6/2016 at 12:13 PM

There is no localStorage plugin - it is built into the webview. For capture, you want cordova-plugin-media-capture.

Comment 15 (In reply to #14) by Maikel Grobbe posted on 6/6/2016 at 12:17 PM

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!

Comment 16 (In reply to #15) by Raymond Camden posted on 6/6/2016 at 12:27 PM

If you remote debug w/ Chrome or Safari, do you see any errors in the console?

Comment 17 by Alessandro Tellini posted on 10/18/2016 at 10:37 PM

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

Comment 18 (In reply to #17) by Raymond Camden posted on 11/7/2016 at 3:46 PM

20 days late - sorry. Afaik you don't have control over the way the sound file is saved.

Comment 19 by Ujjal Saha posted on 11/22/2016 at 2:26 PM

Hi Raymond,
Recorder not working in marshmallow. please help. recorder not open. other android phone is working.

Comment 20 (In reply to #19) by Raymond Camden posted on 11/22/2016 at 3:51 PM

Do you see anything when you remote debug?

Comment 21 (In reply to #20) by Ujjal Saha posted on 11/28/2016 at 7:03 AM

It's working on marshmallow 6.0. but not working on Samsung 7 default
S voice recording.

Comment 22 (In reply to #21) by Raymond Camden posted on 11/28/2016 at 3:58 PM

See my comment - what, if anything, did you see when you remote debugged?

Comment 23 by Fernando posted on 12/7/2016 at 9:54 PM

Hola gracias por compartir

Puedes explicarlo como se hace en la versiĆ³n ultima de Ionic 2. Gracias

Comment 24 (In reply to #23) by Raymond Camden posted on 12/7/2016 at 9:58 PM

No hablo espanol.

Comment 25 by Xiping Long posted on 1/20/2017 at 6:59 AM

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.

Comment 26 (In reply to #25) by Raymond Camden posted on 1/20/2017 at 2:18 PM

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. :)

Comment 27 by Eduardo Lopez posted on 1/30/2017 at 4:24 AM

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);
});

Comment 28 (In reply to #27) by Raymond Camden posted on 1/30/2017 at 12:31 PM

Did you install the plugin?

Comment 29 by Rowdy Akash Bajpai posted on 3/25/2017 at 6:26 AM

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

Comment 30 (In reply to #29) by Raymond Camden posted on 3/26/2017 at 12:36 AM

Do you see anything in Chrome's console when you remote debug?

Comment 31 by Amey Baraskar posted on 7/1/2017 at 10:30 AM

Hi,
Code works Fine
very helpful
Thank you