Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lower iPod volume to play app sound (like the native SMS app)

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?

like image 639
Justin Gallagher Avatar asked Jan 16 '10 18:01

Justin Gallagher


2 Answers

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.

like image 71
yood Avatar answered Sep 21 '22 21:09

yood


The correct way to do what you originally said you wanted to do is your (5) - use ducking. Here's the deal. Use an audio session type that allows other apps to play sound (Playback). Don't turn on ducking until you are about to play your sound. When you are about to play your sound, turn on ducking! The background music will duck immediately, so your sound can be heard. Then, when your sound has finished playing, turn off ducking and now (this is the trick) deactivate your audio session and activate it again, to make the background music resume its former loudness immediately:

UInt32 duck = 0;
AudioSessionSetProperty(kAudioSessionProperty_OtherMixableAudioShouldDuck, sizeof(duck), &duck);
AudioSessionSetActive(false);
AudioSessionSetActive(true);

EDIT: In iOS 6 you can (and should) do all that using just the Objective-C API. So, to start ducking other audio:

[[AVAudioSession sharedInstance] 
    setCategory: AVAudioSessionCategoryAmbient
    withOptions: AVAudioSessionCategoryOptionDuckOthers
          error: nil];

After you finish playing your sound, turn off ducking:

[[AVAudioSession sharedInstance] setActive:NO withOptions:0 error:nil];
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryAmbient
                                 withOptions: 0
                                       error: nil];
[[AVAudioSession sharedInstance] setActive:YES withOptions: 0 error:nil];
like image 42
matt Avatar answered Sep 19 '22 21:09

matt