Cordova/Ionic Sample App: My Sound Board

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.

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also