Objective-C
Xcode
iOS
ARC

ARC環境下でAVAudioPlayerを使い柔軟かつシンプルにサウンドを再生する方法

More than 3 years have passed since last update.

ARC環境下では、以下のコードでサウンドは再生されません。

- (void)playSound{
    NSString *soundPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"hogehoge.mp3";
    NSURL *urlOfSound = [NSURL fileURLWithPath:soundPath];

    AVAudioPlayer* player = [[AVAudioPlayer alloc] initWithContentsOfURL:urlOfSound error:nil];
    [player setNumberOfLoops:0];
    player.delegate = (id)self;
    [player prepareToPlay];
    [player play];
}

なぜなら、メソッドの終了後にplayerのメモリが解放されてしまうからです。
retainをして参照カウントを上げようにも、ARC環境下ではretainは使えません。
この問題に対処するために、AVAudioPlayerのインスタンスをメンバ変数にしようとしても、再生するサウンドの数だけAVAudioPlayerのインスタンスの宣言が必要になり、柔軟なシステムにはなりません。

そして、メンバ変数の場合は同じAVAudioPlayerのインスタンスを再生すると一度それまで同じインスタンスで再生されていた音が停止してから音が再生されるため、音を重ねることが苦手です。
楽器アプリや音ゲーなどで音を再生する際は、音をいくらでも重ねられた方が、自然に聞こえます。

この問題に対処するには、メッソッド内で宣言したAVAudioPlayerのインスタンスを、メンバ変数の配列に格納してやればいいことになります。
サウンドの再生が終了したタイミングで、配列からAVAudioPlayerのインスタンスを削除してやることで、メモリが解放されるのでメモリが圧迫されることもありません。
以下、上記を1つのクラスにまとめたサンプルコードです。

SEManager.h
#import <Foundation/Foundation.h>

@interface SEManager : NSObject{
    NSMutableArray *soundArray;
}

@property(nonatomic) float soundVolume;

+ (SEManager *)sharedManager;
- (void)playSound:(NSString *)soundName;

@end
SEManager.m
#import "SEManager.h"
#import "AVFoundation/AVFoundation.h"

@implementation SEManager

static SEManager *sharedData_ = nil;

+ (SEManager *)sharedManager{
    @synchronized(self){
        if (!sharedData_) {
            sharedData_ = [[SEManager alloc]init];
        }
    }
    return sharedData_;
}

- (id)init
{
    self = [super init];
    if (self) {
        soundArray = [[NSMutableArray alloc] init];
        _soundVolume = 1.0;
    }
    return self;
}

- (void)playSound:(NSString *)soundName{   
    NSString *soundPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:soundName];
    NSURL *urlOfSound = [NSURL fileURLWithPath:soundPath];

    AVAudioPlayer* player = [[AVAudioPlayer alloc] initWithContentsOfURL:urlOfSound error:nil];
    [player setNumberOfLoops:0];
    player.volume = _soundVolume;
    player.delegate = (id)self;
    [soundArray insertObject:player atIndex:0];
    [player prepareToPlay];
    [player play];
}

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
    [soundArray removeObject:player];
}

@end

どのクラスからでもアクセスできるように、シングルトンにしてみました。
実際に他のクラスから音を再生する際は、SEManagerをimportした上で以下のように書きます。

    [[SEManager sharedManager] playSound:@"hogehoge.mp3"];

GitHubにサンプルコードを置いておきますね。
https://github.com/yukinaga/SEManager
SEManagerを使うことで、シンプルな実装で音がいくらでも重なって再生されますのでお試しください。

拙作『ちんあなごのうた 南の海の音楽祭』では、SEManagerを活用して柔軟かつシンプルなサウンドの管理を実現しています。
もしご興味があれば、ぜひ遊んでみてください。
https://itunes.apple.com/jp/app/chinanagonouta-nanno-haino/id611600816?mt=8