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:
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...
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 offadeVolumeOut:
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.
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开发者_C百科 hear.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 likeiPodMusicPlayer
andapplicationMusicPlayer
are sharing the same volume. Is this a result of using[AVAudioSession sharedInstance];
? Is there another way to initialize anAVAudioSession
?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.
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?
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.
Initializing the audio session (error handling code removed):
AVAudioSession* audioSession = [AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil] [audioSession setActive:NO error:nil];
Playing the sound (
AVAudioPlayer
's play/prepareToPlay will activate the audio session for you):AVAudioPlayer* audioPlayer = [[[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil]; [audioPlayer play];
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.
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];
Why not go back to #2? Instead of:
- Fade out
- Play alert
- Fade in
Use:
- Fade out
- Pause music
- Turn volume up
- Play alert
- Turn volume down
- Play music
- Fade in
For Xcode 8, iOS 10
Objective:
To work like in navigation apps (Waze/ Google Maps/ RideAustin) Play your sound, reduce background music volume. After playing sound, restore background music volume
Implementation:
initialize:
{
NSError *error = nil;
[[AVAudioSession sharedInstance]
setCategory: AVAudioSessionCategoryPlayback
withOptions: AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers
error: nil];
[[AVAudioSession sharedInstance] setPreferredIOBufferDuration:0.005 error:&error];
...
}
when playing sound
{
...
NSError *error = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
//set delegate
player.delegate = self
...
[player play]
}
to make sure that other music player app volume will be restored implement
#pragma mark - AVAudioPlayerDelegate
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
...
[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
}
精彩评论