6.2. AVMeter: Build a VU Meter

This example takes advantage of the AVFoundation framework's metering functionality to render a pair of VU (volume unit) meters on the screen as it's playing audio content. The default project plays a file named sample.mp3, and also loads the images avgMeterImage.png and peakMeterImage.png to display the meters. These are all included with the online book examples, or you may provide your own. The example displays a navigation bar with a Play button. After pressing Play, the sample will be played and its meter levels read periodically. The meters on the screen, as shown in Figure 6-1, are updated with the meter readings from the AVAudioPlayer.

Figure 6-1. AVMeter example


You can compile this application, shown in Examples Example 6-1 through Example 6-7, with the SDK by creating a view-based application project named AVMeter. In addition to this, you'll need to add two new files to your project: AVMeterView.h and AVMeterView.m. You can do this by selecting New File from Xcode's File menu, then selecting UIView Subclass from the Cocoa Touch Classes template group underneath iPhone OS.

Be sure to pull out the Interface Builder code so you can see how these objects are created from scratch.

Example 6-1. AVMeter application delegate prototypes (AVMeterAppDelegate.h)
#import <UIKit/UIKit.h>

@class AVMeterViewController;

@interface AVMeterAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UINavigationController *navigationController;
    AVMeterViewController *viewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet AVMeterViewController *viewController;

@end

                                          

Example 6-2. AVMeter application delegate (AVMeterAppDelegate.m)
#import "AVMeterAppDelegate.h"
#import "AVMeterViewController.h"

@implementation AVMeterAppDelegate

@synthesize window;
@synthesize viewController;

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    CGRect screenBounds = [ [ UIScreen mainScreen ] bounds ];

    self.window = [ [ [ UIWindow alloc ] initWithFrame: screenBounds ]
        autorelease
    ];

    viewController = [ [ AVMeterViewController alloc ] init ];
    navigationController = [ [ UINavigationController alloc ]
        initWithRootViewController: viewController
    ];

    [ window addSubview: [ navigationController view ] ];
    [ window makeKeyAndVisible ];
}


- (void)dealloc {
    [ navigationController release ];
    [ viewController release ];
    [ window release ];
    [ super dealloc ];
}
@end

                                          

Example 6-3. AVMeter view controller prototypes (AVMeterViewController.h)
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "AVMeterView.h"

@interface AVMeterViewController : UIViewController {
    UIBarButtonItem *navButton;
    AVAudioPlayer *player;
    AVMeterView *meterView;
}
- (void)navButtonWasPressed;
- (void)setNavProperties;

@end

Example 6-4. AVMeter view controller (AVMeterViewController.m)
#import "AVMeterViewController.h"

@implementation AVMeterViewController

- (id)init {
    self = [ super init ];
    if (self != nil) {
        NSError *err;

        player = [ [ AVAudioPlayer alloc ]
            initWithContentsOfURL: [ NSURL fileURLWithPath: [
                [ NSBundle mainBundle ] pathForResource: @"sample" ofType:@"mp3"
            ] ] error: &err
        ];

        if (err)
            NSLog(@"Failed to initialize AVAudioPlayer: %@\n", err);

        [ self setNavProperties ];
        [ player prepareToPlay ];
    }
    return self;
}

- (void)loadView {
    [ super loadView ];

    CGRect frame = self.view.frame;
    meterView = [ [ AVMeterView alloc ] initWithFrame: frame ];
    meterView.player = player;


    UIImage *peak = [ [ UIImage alloc ] initWithContentsOfFile:  [
        [ NSBundle mainBundle ] pathForResource: @"peakMeterImage" ofType:@"png"
    ] ];
    UIImage *avg = [ [ UIImage alloc ] initWithContentsOfFile:  [
        [ NSBundle mainBundle ] pathForResource: @"avgMeterImage" ofType:@"png"
    ] ];

    meterView.meterPeakPowerImage = peak;
    meterView.meterAveragePowerImage = avg;

    self.view = meterView;
}

- (void)setNavProperties {
    navButton = [ [ [ UIBarButtonItem alloc ]
        initWithTitle: (player.playing == YES) ? @"Stop" : @"Play"
        style: UIBarButtonItemStylePlain
       target: self
       action:@selector(navButtonWasPressed)
    ] autorelease ];
    self.navigationItem.rightBarButtonItem = navButton;

    if (player.playing == YES)
        self.title = @"Playing";
    else
        self.title = @"Stopped";
}

- (void)navButtonWasPressed {
    if (player.playing == YES) {
        [ player stop ];
        [ meterView stopUpdating ];
    } else {
        [ player play ];
        [ meterView startUpdating ];
    }
    [ self setNavProperties ];
}

- (void)dealloc {
    [ super dealloc ];
}
@end

                                          

Example 6-5. AVMeter view prototypes (AVMeterView.h)
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface AVMeterView : UIView {
    AVAudioPlayer *player;
    float cachedAveragePowerForChannel[2], cachedPeakPowerForChannel[2];
    CGRect avgMeterFrame[2], peakMeterFrame[2];
    UIImage *meterAveragePowerImage, *meterPeakPowerImage;
    BOOL updating;

    float meterSpacing;
    float meterHorizontalBorder;
    float meterVerticalBorder;
}
- (id)initWithFrame:(CGRect)frame;
- (void)startUpdating;
- (void)stopUpdating;

@property(nonatomic,assign)    AVAudioPlayer *player;
@property(nonatomic,assign) UIImage *meterAveragePowerImage;
@property(nonatomic,assign) UIImage *meterPeakPowerImage;

@property(nonatomic,assign) float meterSpacing;
@property(nonatomic,assign) float meterHorizontalBorder;
@property(nonatomic,assign) float meterVerticalBorder;

@end

                                          

Example 6-6. AVMeter view (AVMeterView.m)
#import <CoreGraphics/CoreGraphics.h>
#import "AVMeterView.h"

@implementation AVMeterView
@synthesize player;
@synthesize meterAveragePowerImage;
@synthesize meterPeakPowerImage;
@synthesize meterSpacing;
@synthesize meterHorizontalBorder;
@synthesize meterVerticalBorder;

- (id)initWithFrame:(CGRect)frame {
    self = [ super initWithFrame: frame ];
    if (self != nil) {
        player = nil;
        updating = NO;

        meterAveragePowerImage = nil;
        meterPeakPowerImage = nil;

        meterSpacing = 40.0;
        meterHorizontalBorder = 20.0;
        meterVerticalBorder = 20.0;

        for(int i = 0; i < 2; i ++) {
            cachedPeakPowerForChannel[i] = 0.0;
            cachedAveragePowerForChannel[i] = 0.0;
        }

        /* Calculate meter positions */
        avgMeterFrame[0] = CGRectMake(meterHorizontalBorder,
            meterVerticalBorder,
            (frame.size.width / 2) - (meterSpacing/2)
                - meterHorizontalBorder,
            (frame.size.height) - meterVerticalBorder);
        avgMeterFrame[1] = CGRectMake(meterHorizontalBorder
                + (frame.size.width / 2),
            meterVerticalBorder,
            (frame.size.width / 2) - (meterSpacing/2) - meterHorizontalBorder,
            frame.size.height - meterVerticalBorder);

        peakMeterFrame[0] = CGRectMake(avgMeterFrame[0].origin.x,
            avgMeterFrame[0].origin.y,
            avgMeterFrame[0].size.width, meterPeakPowerImage.size.height);
        peakMeterFrame[1] = CGRectMake(avgMeterFrame[1].origin.x,
            avgMeterFrame[1].origin.y,
            avgMeterFrame[1].size.width, meterPeakPowerImage.size.height);
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    float averagePowerForChannel[2], peakPowerForChannel[2];
    BOOL renderedBottom;

    if (player.numberOfChannels < 1) {
        return;
    }

    /* Read meter values */
    if (!player || player.meteringEnabled == NO) {
        averagePowerForChannel[0] = averagePowerForChannel[1] = 0.0;
        peakPowerForChannel[0] = cachedPeakPowerForChannel[0];
        peakPowerForChannel[1] = cachedPeakPowerForChannel[1];
    } else {
        int channels = player.numberOfChannels;
        if (channels > 2)
            channels = 2;
        for(int i = 0; i < channels; i ++)
        {
            float db;
            [ player updateMeters ];
            db = [ player peakPowerForChannel: i ];
            peakPowerForChannel[i] = (50.0 + db) / 50.0;
            db = [ player averagePowerForChannel: i ];
            averagePowerForChannel[i] = (50.0 + db) / 50.0;
        }
        if (channels == 1) {
            peakPowerForChannel[1] = peakPowerForChannel[0];
            averagePowerForChannel[1] = averagePowerForChannel[0];
        }
    }

    /* Either jump to accommodate new level or decrement meters */
    renderedBottom = YES;
    for(int i = 0; i < 2; i ++) {

        if (averagePowerForChannel[i] > cachedAveragePowerForChannel[i])
        {
            cachedAveragePowerForChannel[i] = averagePowerForChannel[i];
        }
        cachedAveragePowerForChannel[i] -= .02;
        if (cachedAveragePowerForChannel[i] < 0) {
            cachedAveragePowerForChannel[i] = 0;
        }

        if (peakPowerForChannel[i] > cachedPeakPowerForChannel[i]) {
            cachedPeakPowerForChannel[i] = peakPowerForChannel[i];
        }
        cachedPeakPowerForChannel[i] -= .01;
        if (cachedPeakPowerForChannel[i] < 0.0)
            cachedPeakPowerForChannel[i] = 0.0;

        if (   cachedPeakPowerForChannel[i] != 0.0
            || cachedAveragePowerForChannel[i] != 0.0)
        {
            renderedBottom = NO;
        }

        if (meterAveragePowerImage) {
          [ meterAveragePowerImage drawAsPatternInRect:
            CGRectMake(avgMeterFrame[i].origin.x,
                avgMeterFrame[i].origin.y + (avgMeterFrame[i].size.height
            -(avgMeterFrame[i].size.height * cachedAveragePowerForChannel[i])),
            avgMeterFrame[i].size.width,
            avgMeterFrame[i].size.height - (avgMeterFrame[i].size.height
            -(avgMeterFrame[i].size.height * cachedAveragePowerForChannel[i])))
          ];
        }

        if (meterPeakPowerImage) {
          [ meterPeakPowerImage drawAsPatternInRect:
            CGRectMake(peakMeterFrame[i].origin.x,
            peakMeterFrame[i].origin.y + (avgMeterFrame[i].size.height
            -(avgMeterFrame[i].size.height * cachedPeakPowerForChannel[i])),
            peakMeterFrame[i].size.width,
                meterPeakPowerImage.size.height)
          ];
        }
    }

    if (updating == YES || renderedBottom == NO) {
        [ NSTimer scheduledTimerWithTimeInterval: 0.01
                                      target: self
                                    selector: @selector(handleTimer:)
                                    userInfo: nil
                                     repeats: NO ];
    }
}

- (void)handleTimer:(NSTimer *)timer {
    [ self setNeedsDisplay ];
}

- (void)startUpdating {
    updating = YES;
    player.meteringEnabled = YES;
    [ self setNeedsDisplay ];
}

- (void)stopUpdating {
    updating = NO;
    player.meteringEnabled = NO;
    [ self setNeedsDisplay ];
}

- (void)dealloc {
    [ super dealloc ];
}
@end

                                          

Example 6-7. AVMeter main (AVMeterView.m)
#import <UIKit/UIKit.h>

int main(int argc, char *argv[]) {

    NSAutoreleasePool * pool = [ [ NSAutoreleasePool alloc ] init ];
    int retVal = UIApplicationMain(argc, argv, nil, @"AVMeterAppDelegate");
    [pool release];
    return retVal;
}

6.2.1. What's Going On

The AVMeter example introduces a reusable view class named AVMeterView. Here's how the example works:

  1. When the application instantiates, the AVMeterAppDelegate class's applicationDidFinishLaunching method is notified. This creates the initial window, view controller, and a navigation controller.

  2. The view controller, AVMeterViewController, creates its own AVAudioPlayer object and instantiates an instance of our custom class, AVMeterView, which is assigned as the controller's active view.

  3. The controller initializes the audio player with a sample sound and attaches it to our custom AVMeterView object by setting its player property. It also loads and assigns images for the average and peak meter graphics.

  4. When the user presses Play, the navButtonWasPressed method is invoked inside the view controller. This instructs the audio player to play, and also invokes the AVMeterView class's startUpdating method.

  5. The AVMeterView class's drawRect method is overridden, and is invoked whenever the screen needs to be updated. The drawRect methods reads the player's meter values and calculates the position of the meter bars. It then calls a timer, which is designed to trigger a screen refresh.

6.2.2. Further Study

  • The new AVFoundation class has some nice features. Be sure to check out its prototypes in your SDK's header files. You'll find these deep within /Developer/Platā forms/iPhoneOS.platform, inside the AVFoundation framework's Headers directory.