AVAudioEngineでiPodライブラリの曲を再生しよう

iOS8から導入されたAVAudioEngineを使えば、Core Audioよりも簡単!と、聞いていたので、iPodの曲を再生させてみました。追加できる音響効果はEQ、リバーブ、ピッチシフト、タイムストレッチなどのAudio Unit由来のものが使えるようです。今回は簡単なクラスAEAudioPlayerControllerを作ってリバーブをかけて再生してみます。

AEAudioPlayerController.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface AEPlayerController : NSObject

+ (instancetype)sharedController;
- (void)play;

@end

AEAudioPlayerController.m

@property (nonatomic) AVAudioEngine *engine;
@property (nonatomic) AVAudioPlayerNode *playerNode;
@property (nonatomic) AVAudioUnitReverb *reverbNode;
@property (nonatomic) AVAudioFile *audioFile;
@property (nonatomic) MPMediaItem *nowPlayingItem;

AVAudioPlayerNodeとはプレイヤーの様なものですが、AVAudioPlayerとは全くの他人
さらにリバーブがAVAudioUnitReverbとなっていてNodeが付いていない

+ (instancetype)sharedController {
    static id sharedController;
    static dispatch_once_t queue = 0;
    dispatch_once(&queue, ^ {
        sharedController = [self new];
    });
    return sharedController;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        self.engine = [AVAudioEngine new];
        self.playerNode = [AVAudioPlayerNode new];
        self.reverbNode = [AVAudioUnitReverb new];
        self.reverbNode.wetDryMix = 40.0f;
        // attach to engine
        [self.engine attachNode:self.playerNode];
        [self.engine attachNode:self.reverbNode];
        // player -> reverb -> mixer
        [self.engine connect:self.playerNode to:self.reverbNode format:self.audioFile.processingFormat];
        [self.engine connect:self.reverbNode to:self.engine.mainMixerNode format:self.audioFile.processingFormat];
        // 適当に再生できる曲をセット
        MPMediaQuery *query = [MPMediaQuery songsQuery];
        [query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:@NO forProperty:MPMediaItemPropertyIsCloudItem]];
        self.nowPlayingItem = query.items[0];
    }
    return self;
}
- (void)play {
    NSURL *url = [self.nowPlayingItem valueForProperty:MPMediaItemPropertyAssetURL];
    self.audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
    [self.engine startAndReturnError:nil];
    [self loadBuffer];
    [self.playerNode play];
}
- (void)loadBuffer {
    uint32_t capacity = 32768;
    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.audioFile.processingFormat frameCapacity:capacity];
    if (buffer != nil) {
        [self.audioFile readIntoBuffer:buffer error:nil];
        [self scheduleBuffer:buffer];
    }
}
- (void)scheduleBuffer:(AVAudioPCMBuffer *)buffer {
    [self.playerNode scheduleBuffer:buffer atTime:nil options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
        [self loadBuffer];
    }];
}

適当な場所で[[AEAudioPlayerController sharedController] play];すると再生開始。loadBufferすると繰り返しloadBufferが呼ばれて少しづつバッファが溜まる仕組みです。iPodの曲はアプリにバンドルされたファイルを再生するのと違って、こうしておかないと再生されないみたい。これなら、ストリーミングにも使えそうでいいかも。ただ、プレイヤーとして完成させるには、まだまだ機能不足。それでも再生だけならCore Audioより楽かも。

EQも挟むならこんな感じになります。

// player -> eq -> reverb -> mixer
[self.engine connect:self.playerNode to:self.eqNode format:self.audioFile.processingFormat];
[self.engine connect:self.eqNode to:self.reverbNode format:self.audioFile.processingFormat];
[self.engine connect:self.reverbNode to:self.engine.mainMixerNode format:self.audioFile.processingFormat];

iOS8.1

Xcode6でプロジェクトにpchファイルを追加する

Xcode6からpchファイルが新規プロジェクトには含まれなくなりましたので、追加する方法をご紹介。

まず、「New File…」で「Prefix.pch」を追加します。
ファイル名は自由に決められますが、ここではPrefixで。

2015-03-30 14.24.15

2015-03-30 14.24.28

2015-03-30 14.24.40

ファイルを追加しただけでは機能しないので、ファイルの場所を設定しておきます。
「Build Settings > Apple LLVM 6.0 – Language > Prefix Header」の行に「$(PRODUCT_NAME)/Prefix.pch」を設定します。

2015-03-30 14.27.32

2015-03-30 14.27.37

iOSアプリ開発に便利なマクロ

Prefix.pchに定義しておくと便利なマクロです。
ファイルが見当たらない場合は次の記事も合わせてどうぞ。

短縮NSLog

#define LOG(format, ...) (NSLog(@"%s(L%d) %@", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat:format, ##__VA_ARGS__]))

NSLogと同じ使い方で「メソッド名」「行数」も追加して出力します。

設定言語判定

#define SYSTEM_LANGUAGE ([NSLocale preferredLanguages][0])
if ([SYSTEM_LANGUAGE isEqualToString:@"ja"]) {
    // 日本語で使用中
}

iOSバージョン判定

#define SYSTEM_VERSION_COMPARE(comparator, version) ([[UIDevice currentDevice].systemVersion compare:version options:NSNumericSearch] comparator NSOrderedSame)

比較演算子で分かりやすく分岐できます。

if (SYSTEM_VERSION_COMPARE(>=, @"4.2.10")) {
    // iOS4.2.10以上
}

iPhone or iPad デバイス判定

#define IS_IPAD ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
#define IS_IPHONE ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone)

条件の左辺はマクロが定義されているので、UI_USER_INTERFACE_IDIOM()に書き換えても同じです。

[UIDevice currentDevice].userInterfaceIdiom

の部分は新しい書き方だと

[UIScreen mainScreen].traitCollection.userInterfaceIdiom