yoppa.org


Blog

iPodライブラリ内の曲をopenFrameworksで読み込んでサウンドレベルを取得する

picker.png

iPhone開発修行、ちょっと進展したのでご報告。

現在オーディオ・ビジュアル表現のためのアプリを作成したいと考えていて、やはりこういったジャンルはiPhone用のopenFrameworksを利用するほうが効率が良さそう。openFrameworksでサウンドを活用する場合、あらかじめリソースの中に.caf形式のサウンドファイルを入れておいて、それを参照するやり方が一番標準的な方法だ。また、マイク入力を利用してリアルタイムのサウンドストリームを利用することも可能となっている。

ところが、openFrameworksはそのままではiPhone内のiPodライブラリ内の曲のデータは参照できない。せっかくなら、自分のiPhoneに入れた音楽を使用していろいろ面白いビジュアライズをやってみたい。そんなわけで、iOSのSDKを直に利用してiPodライブラリ内のサウンドデータを参照する方法がないものか探ってみた。

iPodライブラリの内容を参照し、曲をピックアップする方法は比較的簡単だった。iOS SKDにはMPMediaPickerControllerというiPodライブラリの曲を選択するためのUIを提供するクラスがある。これを活用するとすぐに高機能な曲目ピッカーが活用できる。openFrameworksでMPMediaPickerControllerを利用するには、以前このブログの「iOSのGUIをopenFrameworksのプロジェクトに追加する」で紹介した方法で、oFにUIViewControllerのサブクラスを追加して、そのUIViewControllerのサブクラスにMPMediaPickerControllerのインスタンスを生成して、delegateしてあげれば良い。MPMediaPickerControllerを利用するには、プロジェクトにMediaPlayer.frameworkを追加する必要がある。また、MPMediaPickerControllerはiOSシミュレータでは動かすことはできない。必ず実機で動かすようにして欲しい。

ここで取得したアイテムはMPMediaItemクラスのインスタンスとして取得される。これを、MPMusicPlayerControllerというプレイヤーのクラスに読みこんでplayしてあげれば、それだけで曲は再生できる。非常に楽ちん。

ところが、MPMusicPlayerControllerは曲の再生はポーズなどのコントロールはできるものの、サウンドデータそのものには全くアクセスできない。ここでしばらく悩んでしまった。いろいろググったところ、素晴しい記事を発見。

詳細はこの2つの記事を参考にしてほしいのだが、ポイントとしては、MPMediaItemからiPodのリソースのURLを取得した上で、普通のオーディオファイルとして書きだしている。

書き出した生のオーディオデータを再生するには、AVAudioPlayerクラスを活用するのが便利なようだ。AVAudioPlayerでサウンドを再生し、そのレベルを取得するメソッドを追加してその値をoFのtestAppから見られるようにしてみた。これで、あとはoFの方でいろいろ工夫すれば、iPodの音楽を活用したオーディオビジュアルなアプリができるはず。

ここまでの内容のソースコードにまとめてみた。MPPikerクラスが、iPodのライブラリを読み込んで再生する部分。

testApp.h

#pragma once

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

#include "ofMain.h"
#include "ofxiPhone.h"
#include "ofxiPhoneExtras.h"

class testApp : public ofxiPhoneApp {
	
public:
	void setup();
	void update();
	void draw();
	void exit();
	
	void touchDown(ofTouchEventArgs &touch);
	void touchMoved(ofTouchEventArgs &touch);
	void touchUp(ofTouchEventArgs &touch);
	void touchDoubleTap(ofTouchEventArgs &touch);
	void touchCancelled(ofTouchEventArgs &touch);

	void lostFocus();
	void gotFocus();
	void gotMemoryWarning();
	void deviceOrientationChanged(int newOrientation);
};

testApp.mm

#include "testApp.h"
#include "MPPicker.h"

MPPicker *picker;

void testApp::setup(){
    ofSetFrameRate(30);
    ofEnableAlphaBlending();
    ofSetCircleResolution(32);
	ofRegisterTouchEvents(this);
	ofxAccelerometer.setup();
	ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    picker = [[MPPicker alloc] initWithNibName:@"MPPicker" bundle:nil];
    [ofxiPhoneGetUIWindow() addSubview:picker.view];
}

void testApp::update(){

}

void testApp::draw(){
    //左右のサウンドレベルを取得
    float levelL = [picker getLevelWithChannel:0];
    float levelR = [picker getLevelWithChannel:1];

    //サウンドレベル半径にして円を描画
    ofSetColor(0, 63, 255, 127);
    ofTranslate(ofGetWidth()/3, ofGetHeight()/2);
    ofEllipse(0, 0, levelL * 400.0, levelL * 400.0);
    ofTranslate(ofGetWidth()/3, 0);
    ofEllipse(0, 0, levelR * 400.0, levelR * 400.0);
    
}

void testApp::exit(){ }
void testApp::touchDown(ofTouchEventArgs &touch){ }
void testApp::touchMoved(ofTouchEventArgs &touch){ }
void testApp::touchUp(ofTouchEventArgs &touch){ }
void testApp::touchDoubleTap(ofTouchEventArgs &touch){ }
void testApp::lostFocus(){ }
void testApp::gotFocus(){ }
void testApp::gotMemoryWarning(){ }
void testApp::deviceOrientationChanged(int newOrientation){ }
void testApp::touchCancelled(ofTouchEventArgs& args){ }

MPPicker.h

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

#include "testApp.h"

@interface MPPicker : UIViewController <MPMediaPickerControllerDelegate, UITableViewDelegate, AVAudioPlayerDelegate> {
    testApp *myApp;
    AVAudioPlayer *player;
    IBOutlet UIBarButtonItem *showPickerButton;
    IBOutlet UIBarButtonItem *playButton;
    IBOutlet UIBarButtonItem *pauseButton;
}

-(IBAction)showMediaPicker:(id)sender;
-(IBAction)playMediaItem:(id)sender;
-(IBAction)pauseMediaItem:(id)sender;
-(float)getLevelWithChannel:(int)ch;

@end

MPPicker.mm

#import "MPPicker.h"

@implementation MPPicker

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
    }
    return self;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}


// iPodの曲目のピッカーを表示
-(IBAction)showMediaPicker:(id)sender
{
    MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeAnyAudio];
    [picker setDelegate: self];
    [picker setAllowsPickingMultipleItems: NO];
    picker.prompt = NSLocalizedString (@"Add songs to play", "Prompt in media item picker");

    // pickerをModalViewに表示
    [self presentModalViewController: picker animated: YES];
    [picker release];
    [player stop];
}

// Pickerで取得したデータをファイルに書き出し
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
{
    MPMediaItem *item = [mediaItemCollection.items lastObject];
    NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:nil];
    
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc]
                                           initWithAsset:urlAsset
                                           presetName:AVAssetExportPresetPassthrough];
    
    
    NSArray *tracks = [urlAsset tracksWithMediaType:AVMediaTypeAudio];
    AVAssetTrack *track = [tracks objectAtIndex:0];
    id desc = [track.formatDescriptions objectAtIndex:0];
    const AudioStreamBasicDescription *audioDesc = CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)desc);
    FourCharCode formatID = audioDesc->mFormatID;
    
    NSString *fileType = nil;
    NSString *ex = nil;
    
    switch (formatID) {
            
        case kAudioFormatLinearPCM:
        {
            UInt32 flags = audioDesc->mFormatFlags;
            if (flags & kAudioFormatFlagIsBigEndian) {
                fileType = @"public.aiff-audio";
                ex = @"aif";
            } else {
                fileType = @"com.microsoft.waveform-audio";
                ex = @"wav";
            }
        }
            break;
            
        case kAudioFormatMPEGLayer3:
            fileType = @"com.apple.quicktime-movie";
            ex = @"mp3";
            break;
            
        case kAudioFormatMPEG4AAC:
            fileType = @"com.apple.m4a-audio";
            ex = @"m4a";
            break;
            
        case kAudioFormatAppleLossless:
            fileType = @"com.apple.m4a-audio";
            ex = @"m4a";
            break;
            
        default:
            break;
    }
    
    exportSession.outputFileType = fileType;
    
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *filePath = [[docDir stringByAppendingPathComponent:[item valueForProperty:MPMediaItemPropertyTitle]] stringByAppendingPathExtension:ex];
    exportSession.outputURL = [NSURL fileURLWithPath:filePath];
        
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            NSLog(@"export session completed");
            [exportSession release];
            player = [[AVAudioPlayer alloc] initWithContentsOfURL:exportSession.outputURL error:nil];
            player.meteringEnabled = YES;
            [player play];
        } else {
            NSLog(@"export session error");
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Export session error\nPlease select again" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
            [alert show];
        }
    }];
    [self dismissModalViewControllerAnimated:YES];
}

// 曲をキャンセルした時
- (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
	[self dismissModalViewControllerAnimated:YES];
}

// 曲を再生
-(IBAction)playMediaItem:(id)sender
{
    [player play];
}

// 曲をポーズ
-(IBAction)pauseMediaItem:(id)sender
{
    [player pause];
}

// チャンネルを指定して、再生レベルを取得
-(float)getLevelWithChannel:(int)ch
{
    if(player.playing){
        [player updateMeters];
        float db = [player averagePowerForChannel:ch];
        float power = pow(10, (0.05 * db));
        return power;
    } else {
        return nil;
    }
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
    myApp = (testApp*)ofGetAppPtr();
    player.delegate = self;
}


- (void)viewDidUnload
{
    [super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    //return (interfaceOrientation == UIInterfaceOrientationPortrait);
    return NO;
}

@end

Box2Dと組みあわせて、物理シミュレーションを使用したビジュアライザーや、ofImagePickerを使用してカメラ入力の画像を音でモジュレーションするアプリなどいろいろ夢が膨らむ。もう少しoFとの連携を強化したものを、addonにして配布したら感謝されるんだろうか? ofxMPMediaPickerとか。

プロジェクトをダウンロード

下記のリンクよりプロジェクトファイル一式がダウンロードできます。of_preRelease_v007_iphone、XCode 4.1 + iOS SDK 3.2で作成しています。