Episode #43

AVAudioPlayer

14 minutes
Published on November 29, 2012

This video is only available to subscribers. Get access to this video and 572 others.

The iOS SDK has numerous ways to play back audio. In this episode we take a look at how to play a local mp3 file using AVAudioPlayer. We add play/pause support, volume, and show the song progress in a UISlider. We finish it off by monitoring the audio levels using a custom view.

Episode Links

Loading & Playing a local audio file

  self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[self localAudioFileURL]
                                                              error:nil];

  [self.audioPlayer play];

Changing the volume of the player

Changing the volume is really easy with AVAudioPlayer...

- (void)onVolumeSliderChanged:(id)sender {
  self.audioPlayer.volume = self.volumeSlider.value;
}

Monitoring Song Progress

You can watch the song progress in one of two ways:

  • Observe the currentTime property using KVO
  • Read the values in an NSTimer loop

Both methods are fairly straight-forward. Here is how you might accomplish this using NSTimer:

- (void)viewDidLoad {
   ...

   [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(onTimerTick:) userInfo:nil repeats:YES];
}

- (void)onTimerTick:(id)sender {
      if (![self.audioPlayer isPlaying]) {
        return;
    }

    NSTimeInterval totalTime = [self.audioPlayer duration];
    NSTimeInterval currentTime = [self.audioPlayer currentTime];
    CGFloat progress = currentTime / totalTime;
    self.songProgressView.progress = progress;
}

Monitoring audio levels

Using the same timer technique, we can watch the audio levels. To get the smoothest looking graph, you'd have to update at 60 fps, which means your timer would have an interval of 1/60.0f or 0.016 seconds. In the video, I just chose 0.1 (it was a nice round number). Just remember that any code you execute in these tight loops could actually take longer than it would take to process the next frame.

- (void)viewDidLoad {
    ...

    [NSTimer scheduledTimerWithTimeInterval:1/60.0f target:self selector:@selector(checkAudioLevels:) userInfo:nil repeats:YES];
}

- (void)checkAudioLevels:(id)sender {
    if (![self.audioPlayer isPlaying]) {
        return;
    }

    [self.audioPlayer updateMeters];
    [self.audioLevelsView setNumberOfChannels:self.audioPlayer.numberOfChannels];

    for (int c=0; c<self.audioPlayer.numberOfChannels; c++) {
        float level = [self.audioPlayer averagePowerForChannel:c];

        // 0    LOUD
        // -160 SILENT
        [self.audioLevelsView setLevel:level forChannel:c];
    }
}

Note that the audio levels come back in decibels, on a scale of -160db to 0db. 0db is the loudest setting and -160db is complete silence. So in order you render this as a bouncing bar, you have to calculate how loud it is in a different scale. By adding 160 and then dividing by 160, you get a nice number between 0 and 1 which we can use to figure out exactly how tall to render the bar.

Rendering audio levels with a custom view

#define MAX_CHANNELS 8

@implementation AudioLevelsView {
    float levels[MAX_CHANNELS];
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.numberOfChannels = 1;
        for (int i=0; i<MAX_CHANNELS; i++) {
            levels[i] = 0;
        }
    }
    return self;
}

- (void)setLevel:(CGFloat)level forChannel:(NSInteger)channel {
    NSAssert(channel <= 0 && channel < MAX_CHANNELS, @"Invalid channel: %d", channel);
    levels[channel] = level;
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGFloat margin = self.numberOfChannels > 1 ? 0 : 2;
    for (int c = 0; c<self.numberOfChannels; c++) {
        float width = self.frame.size.width / self.numberOfChannels - margin;
        float x = width * c;
        float height = self.frame.size.height;
        CGContextSetFillColorWithColor(context, [[UIColor blackColor] CGColor]);
        CGContextFillRect(context, CGRectMake(x, 0, width, self.frame.size.height));

        CGContextSetFillColorWithColor(context, [[UIColor greenColor] CGColor]);

        float normalizedLevel = ((levels[c] + 160) / 160.f) * 0.85;
        float levelHeight = height * normalizedLevel;
        CGContextFillRect(context, CGRectMake(x, height - levelHeight, width, levelHeight));
    }
}

@end