问题
This is the code I'm using now, and it's not working (nothing happens when I press the button that calls this method). Previously, I had a property for audioPlayer and it worked (all the audioPlayers below were self.audioPlayer obviously). The problem was that when I tried to play the sound twice, it would end the first sound playing.
This is no good because I'm making a soundboard and want sounds to be able to overlap. I thought I could just make audioPlayer a local variable instead of a property and all would be ok but now the sound doesn't work at all and I can't figure out why. In all tutorials I've found for AVAudioPlayer, a property is made but no one explains why. If this can't work, what alternatives do I have to make sounds that can overlap?
- (void)loadSound:(NSString *)sound ofType:(NSString *)type withDelegate:(BOOL)delegate {
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle]
pathForResource:sound
ofType:type]];
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
if (delegate) audioPlayer.delegate = self;
[audioPlayer prepareToPlay];
[audioPlayer play];
}
回答1:
The reason you need a property or ivar is for the strong reference it provides. When using ARC any object without a strong pointer to it is fair game for deallocation, and in fact that is what you are seeing.
You are also correct that an AVAudioPlayer
strong pointer will only allow one audio player to be referenced at a time.
The solution, if you choose to continue to use AVAudioPlayer
is to use some sort of collection object to hold strong reference to all the player instances. You could use an NSMutableArray
as shown here:
Edit I tweaked the code slightly so method that plays the sound takes an NSString
soundName
parameter.
@synthesize audioPlayers = _audioPlayers;
-(NSMutableArray *)audioPlayers{
if (!_audioPlayers){
_audioPlayers = [[NSMutableArray alloc] init];
}
return _audioPlayers;
}
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
[self.audioPlayers removeObject:player];
}
-(void)playSoundNamed:(NSString *)soundName{
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle]
pathForResource:soundName
ofType:@"wav"]];
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
if (audioPlayer){
[audioPlayer setDelegate:self];
[audioPlayer prepareToPlay];
[audioPlayer play];
[self.audioPlayers addObject:audioPlayer];
}
}
Generally an AVAudioPlayer
is overkill for an sound-effect/soundboard application. For quick sound "drops" you will likely find the audio toolbox framework, as outlined in my answer to this question.
From looking at the System Sound class reference, it seems like you can only play one sound at a time.
It can only play one SystemSoundID
at a time. So for example if you have soundOne and soundTwo. You can play soundOne while soundTwo is playing, but you cannot play more than one instance of either sound at a time.
What's the best way to be able to play sounds that can overlap while still being efficient with the amount of code and memory?
Best is opinion.
If you need two instances of the same sound to play at the same time, then I would say the code posted in this answer would be the code to use. Due to the fact that each overlapping instance of the same sound requires creating a new resource, code like this with its audioPlayerDidFinishPlaying:
is much more manageable(the memory can easily be reclaimed).
If overlapping instances of the same sound are not a deal-breaker then I think just using AudioServicesCreateSystemSoundID()
to create one instance of each sound is more efficient.
I definitely would not try to manage the creation of and disposal of SystemSoundID
s with each press of a button. That would go wrong in a hurry. In that instance AVAudioPlayer
is the clear winner on just maintainability alone.
回答2:
I am assuming you are using ARC. The reason that the audio player doesn't work is because the AVAudioPlayer
object is being released and then subsequently destroyed once the loadSound:
method terminates. This is happening due to ARC's object management. Before ARC, the code:
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
if (delegate) audioPlayer.delegate = self;
[audioPlayer prepareToPlay];
[audioPlayer play];
would play the sound as expected. However, the AVAudioPlayer
object would still exist long after the loadSound:
method terminates. That means every time you play a sound, you would be leaking memory.
A little something about properties
Properties were introduced to reduce the amount of code the developer had to write and maintain. Before properties, a developer would have to hand write the setters and getters for each of their instance variables. That's a lot of redundant code. A fortunate side-effect of properties was that they took care of a lot of the memory management code needed to write setters/getters for object-based instance variables. This meant that a lot of developers started using properties exclusively, even for variables that didn't need to be public.
Since ARC handles all the memory management details for you, properties should only be used for their original purpose, cutting down on the amount of redundant code. Traditional iVars will be strongly referenced by default, which means a simple assignment such as:
title = [NSString stringWithFormat:@"hello"];
is essentially the same as the code:
self.title = [NSString stringWithFormat:@"hello"];
.
OK, back to your question.
If you are going to be creating AVAudioPlayer
instances in the loadSound:
method, you'll need to keep a strong reference to each AVAudioPlayer
instance or else ARC will destroy it. I suggest adding the newly created AVAudioPlayer
objects into a NSMutableArray
array. If you adopt the AVAudioPlayerDelegate
protocol, you can implement the audioPlayerDidFinishPlaying:successfully:
method. In which you can remove the AVAudioPlayer
object from the array, letting ARC know that it's OK to destroy the object.
来源:https://stackoverflow.com/questions/13485536/avaudioplayer-must-you-create-a-property-for-it-to-work-xcode