views:

666

answers:

3

I've made an iPhone app to be used while exercising. It plays a bell tone to indicate that you (the user) should switch from one step of your exercise routine to the next. I've designed the app so that you can listen to music on the iPod while using the app, and I want the tone to play sufficiently audibly over the music. I've gotten this to work...sort of...

When the music is loud, it's hard to hear the tone. My ideal solution is something similar to the way the iPod app handles an incoming text message or email. The music volume lowers, the sound plays, then the music volume fades back in.

Here are the approaches that I've tried so far:

  1. I've used AudioServicesPlaySystemSound to play the sound.

    I initialized the sound like this:

    CFBundleRef mainBundle = CFBundleGetMainBundle();
    soundFileURLRef = CFBundleCopyResourceURL(mainBundle, CFSTR ("bell"), CFSTR ("wav"), NULL);
    AudioServicesCreateSystemSoundID (soundFileURLRef, &soundFileID);
    

    And I play the sound at the appropriate time using:

    AudioServicesPlaySystemSound (self.soundFileID);
    

    This plays the sound fine, but it is too hard to hear over loud music. On to attempt 2...

  2. I tried to lower the iPod volume, play the sound, and then return the volume to its previous level.

    If there's 1 second left in the current step, start lowering the volume:

    if ([self.intervalSet currentStepTimeRemainingInSeconds] == 1) {
        self.volumeIncrement = originalVolume/5.0f;
        [NSTimer scheduledTimerWithTimeInterval:0.5f 
                                         target:self 
                                       selector:@selector(fadeVolumeOut:) 
                                       userInfo:[NSNumber numberWithInt:1]
                                        repeats:NO];
    }
    

    Here's the fadeVolumeOut: method:

    - (void) fadeVolumeOut:(NSTimer *)timer {
        if (!TARGET_IPHONE_SIMULATOR) {
            NSNumber *volumeStep = (NSNumber *)[timer userInfo];
            int vStep = [(NSNumber *)volumeStep intValue];
            float volume = [[MPMusicPlayerController iPodMusicPlayer] volume];
            volume = volume - self.volumeIncrement;
            if (volume < 0.0f) {
                volume = 0.0f;
            }
            [[MPMusicPlayerController iPodMusicPlayer] setVolume:volume];
            if (vStep < 5) {
                vStep = vStep + 1;
                [NSTimer scheduledTimerWithTimeInterval:0.1f 
                                                 target:self 
                                               selector:@selector(fadeVolumeOut:) 
                                               userInfo:[NSNumber numberWithInt:vStep]
                                                repeats:NO];
            }
        }
    }
    

    Then, when the step ends, play the alert sound and fade the volume back in:

     [NSTimer scheduledTimerWithTimeInterval:0.1f 
                                      target:self 
                                    selector:@selector(alertAndFadeVolumeIn) 
                                    userInfo:nil
                                     repeats:NO];
    

    Here's the alertAndFadeVolumeIn method:

     - (void) alertAndFadeVolumeIn {
         [self alert];
         [NSTimer scheduledTimerWithTimeInterval:0.25f 
                                          target:self 
                                        selector:@selector(fadeVolumeIn:) 
                                        userInfo:[NSNumber numberWithInt:1]
                                         repeats:NO];
     }
    

    And fadeVolumeIn: is basically the opposite of fadeVolumeOut: above.

    This works, the volume fades out, the sound plays, and the volume fades back in. The problem is that the tone volume is lowered by the same amount as the iPod, so it doesn't make it any easier to hear over the music.

  3. I switched to AVAudioSession to play the sound, and set up the session so that the iPod music will continue to play while the app is in use. Here's how I'm initializing the session:

     AVAudioSession *session = [AVAudioSession sharedInstance];
     [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    
    
     OSStatus propertySetError = 0;
     UInt32 allowMixing = true;
     propertySetError = AudioSessionSetProperty (
         kAudioSessionProperty_OverrideCategoryMixWithOthers,
         sizeof (allowMixing),
         &allowMixing
     );
    
    
     NSError *activationError = nil;
     [session setActive:YES error:&activationError];
    
    
     NSString *audioFile = [[NSBundle mainBundle] pathForResource:@"bell"
                                                           ofType:@"wav"];
     player = [[AVAudioPlayer alloc] initWithContentsOfURL:
         [NSURL fileURLWithPath:audioFile] error:NULL];
    

    To play the sound, I call [self.player play] at the appropriate time. Again, the tone volume lowers along with the iPod volume, and the tone is not any easier to hear.

  4. I tried putting [[MPMusicPlayerController applicationMusicPlayer] setVolume:1.0f]; right before the alert sound plays. This had mixed results. The first time the sound plays at full volume as I had hoped, but subsequent times the volume is much lower. Also, the music doesn't fade out smoothly. It seems like iPodMusicPlayer and applicationMusicPlayer are sharing the same volume. Is this a result of using [AVAudioSession sharedInstance];? Is there another way to initialize an AVAudioSession?

  5. Next, I tried using AVAudioSession "ducking":

     OSStatus propertySetError = 0;
     UInt32 allowDucking = true;
     propertySetError = AudioSessionSetProperty (
         kAudioSessionProperty_OtherMixableAudioShouldDuck,
         sizeof (allowDucking),
         &allowDucking
     );
    

    Unfortunately, the iPod music "ducks" when the audio session is activated, which is as soon as the viewController is loaded the way I had things.

  6. Finally, I changed my code so that the audio session is activated one second before the step ends, the sound is played when the step ends, and one second later, the session is deactivated. I've removed all of my fading in and out code at this point. Here are the relevant code snippets:

    if ([self.intervalSet currentStepTimeRemainingInSeconds] == 1) {
        AVAudioSession *session = [AVAudioSession sharedInstance];
        [session setActive:YES error:nil];
    }
    

    ...

    if (self.shouldRing) {
        [self.player play]; 
        [NSTimer scheduledTimerWithTimeInterval:1.0f 
                                         target:self 
                                       selector:@selector(stopSound)
                                       userInfo:nil
                                        repeats:NO];
    }
    

    ...

    - (void) stopSound {
        [self.player stop];
        AVAudioSession *session = [AVAudioSession sharedInstance];
        [session setActive:NO error:nil];
    }
    

    This takes me to the point where I'm really stuck. This works perfectly after the first step ends. The iPod volume ducks, the sound plays loudly, and the iPod volume fades back in a second later. However, the second time a step ends the sound plays slightly less loudly, the third time it's barely audible, and the fourth time it's silent. Then, the fifth time, it plays loudly again and the cycle repeats.

    On top of that, activating and deactivating seem to be pretty heavy operations and they cause the timer to stutter slightly.

Has anyone tried to do something similar? Is there a preferred approach to playing a sound over the iPod music?

+3  A: 

Why not go back to #2? Instead of:

  1. Fade out
  2. Play alert
  3. Fade in

Use:

  1. Fade out
  2. Pause music
  3. Turn volume up
  4. Play alert
  5. Turn volume down
  6. Play music
  7. Fade in
David Kanarek
Thanks for the response, David. I've considered this. However, this approach doesn't quite work. Here's what happens: The music fades out, the alert plays *and the music plays at that volume too*, the volume goes back to zero, and then the music fades back in.
Justin Gallagher
So essentially, you can't pause the music, or it resumes before the volume is turned down again despite coming afterward programmatically?
David Kanarek
Hmmm, I hadn't considered completely pausing the music. I could give that a shot. I was hoping to keep the music playing, just at a reduced volume. But maybe this will be a good work around...
Justin Gallagher
Decent workaround. Shame though: lowering the music volume, playing an alert, and re-raising the music volume is what Apple’s apps (e.g. SMS) do, because that’s the best experience for the user. Sucks if 3rd-party apps can’t also do that.In fact, it kinda sucks that there isn’t a method call in the API that does that and only that, in case they’re worried that 3rd-party apps shouldn’t have access to music volume.
Paul D. Waite
A: 

I'm trying to do the same thing. But I was no able so far.

I know it's possible on 3rd party apps because Navigon does it. It fades out the iPod music to play directions then fades in back the music.

Salvador
+2  A: 

AVAudioSession is the proper way to handle "audio behavior at the application, interapplication, and device levels." I used a slightly modified #6 on iOS 4 with no problems. The background audio faded out, my sound played, and the background audio faded back in.

  1. Initializing the audio session (error handling code removed):

    AVAudioSession* audioSession = [AVAudioSession sharedInstance];
    [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil]
    [audioSession setActive:NO error:nil];
    
  2. Playing the sound (AVAudioPlayer's play/prepareToPlay will activate the audio session for you):

    AVAudioPlayer* audioPlayer = [[[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil];
    [audioPlayer play];
    
  3. Stopping the sound:

    [audioPlayer stop];
    [[AVAudioSession sharedInstance] setActive:NO withFlags:AVAudioSessionSetActiveFlags_NotifyOthersOnDeactivation error:nil];
    

    The flag kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation (new to iOS 4) tells the system to notify background audio to resume playing.

yood
This was a perfect solution for me, since I was playing an audio (speech) file through the AVAudioPlayer. Thanks!
Joe