22
Mar
2008
 

Cocoa Tutorial: Audio Scrub

by Matt Long

I am currently working on an application that needs to set markers in audio and video tracks. While it’s easy to find a marker visually in the video tracks, it wasn’t quite so clear as to how to set a marker in an audio track. In this tutorial, I’ll demonstrate how to create an audio scrub utility that will play a short audio clip when you drag an NSSlider. It shows the current time code of the track while you update the slider.

You can download the demo project for this post here: Audio Scrub Demo Application

You may be wondering why you might want something like this. Here’s my reasoning. I sometimes set up to record audio through a high-end microphone while also video taping an event. The high end microphone doesn’t plug into my camcorder, so I just run it into another audio recording device. This way, I have a high-quality audio track to go along with the video. The problem then is that I need a way to merge the audio track with the video track. In my resulting video, I only want one audio track, so I need to overwrite the original audio track from the video clip with the audio track from the high-quality audio recording. It is necessary to match the track times to get it to sound/look right.

I will not go into any further depth as to what my application will do. Suffice it to say that to merge the high-quality audio track with the video, I have to set a marker in both tracks that will allow me to align them. That’s where the need for an audio scrubber comes in. I need to find the same marker (hand clap, other loud noise) in both files and set the two tracks to have that marker as their matching starting points.

Timers and Selectors with Timeout

QTKit provides this great little abstraction called QTMovie. It allows you to load up any media file and call play: on it and just starts to playback. What I wanted was for the audio to start playing back when the slider changed, but then to stop after a period of time so that it was just playing a quick sample on each change. My first approach was to use an NSTimer, but then I found something a little simpler.

- (IBAction)scrubAudio:(id)sender;
{
    if( !movie )
        return;
    
    QTTime currentTime;
    NSTimeInterval sliderTime = [sender floatValue];
    
    currentTime.timeValue = movieDuration.timeValue * sliderTime;
    currentTime.timeScale = movieDuration.timeScale;
    currentTime.flags = 0;

    // Set the current time of the movie based upon the slider
    // position
    [movie setCurrentTime:currentTime];
    
    // Update our time code field.
    [self updateTimeTextField];

    // Start playback
    [movie setRate:1.0];
    
    isPlaying = YES;
    
    // Let the audio play for nearly a second and then call checkPlayback which
    // will stop playback. This gives the user a chance to hear a sample of the 
    // audio clip at the current time.
    [self performSelector:@selector(checkPlayback:) withObject:nil afterDelay:0.95];
    
}

This IBAction is connected to the slider in Interface Builder. Notice specifically the performSelector call. We’ll get into more depth with that call in a minute.

Srubbing Without Bubbles

Audio Scrub

I’m sure there are other ways to achieve an audio scrubbing feature, however, I was looking for the path of least resistance and using a QTMoive object seemed the best way to do that.

Now, here is what I figured was the most logical way to solve this problem.

  • When the scrubber moves, an audio sample should be played from current position in the audio file
  • When the audio sample starts to play, it should only play for a moment and then stop playing
  • When the next or previous buttons are clicked, the sample at that point should be played and then stopped, but the playhead should be positioned at the next position though playback rolled ahead.

To keep track of whether the audio is playing currently, I use a boolean flag called isPlaying. This gets toggled when the audio sample is set to play. It then gets toggled again when the playback gets stopped.

Perform Selector After Delay

When playback gets initiated, I call performSelector:withobject:afterDelay. This is a really handy little function that allows us to call another function after the specified delay. In the case of using the slider, we simply call checkPlayback: without any arguments. In the case of clicking the next or previous buttons, we pass in a QTTime object as an argument as we want the current time of playback to be set back to this time though playback has actually rolled ahead. Here is the implementation of checkPlayback:.

- (void)checkPlayback:(id)previousTime;
{
    if( movie )
    {
        // If it's still playing back...
        if( [movie rate] > 0.0 )
        {
           // stop playback
            [movie stop];

            // If the previousTime object was passed in, set
            // our currentTime in the audio file (movie) to use it.
            if( previousTime )
                [movie setCurrentTime:[previousTime QTTimeValue]];
            [self updateSlider];
            isPlaying = NO;
        }
    }
}

NSSlider Considerations

The demo project has this set up already, but you need to understand that there are a few important details when creating your slider in Interface Builder. The first thing is that you need to make sure you minimum and maximum values are set to 0.0 and 1.0 respectively. This will give you the fractional number you need to calculate the position in the audio track where the current time is. Next, you need to make sure that continuous updating is enabled by checking the Continuous property. See the screenshot below. Continuous updating means that the IBAction for the slider gets called while you are dragging the slider. If you left this unchecked, it would only be called when you released your mouse button.
Slider Attributes

Conclusion

If you know of a better way to achieve this affect, please let me know. I feel this method I’m using works adequately well, however, I’m sure there is room for improvement.