icon 2011/12/24 MusicPlayer を使ってみる
今回思い切って、iOS Advent Calendar2011 に参加してみました。
しかも最終日自分でいいのか?と思いましたが・・・最終回のネタは「MusicPlayer」です。
MusicPlayer
そして25日ということで、

クリスマスソングを再生させていただきます



MusicPlayer は OSX には既に存在していますが iOS5 から AudioToolbox.framework に追加された機能で、簡単に言うと音楽シーケンサーです。
MusicPlayer
ドキュメントを見る限り、OSX の機能はフルには使えないようですが、基本的な機能は利用できます。

MusicPlayer の有利性


個人的に作ったアプリ「Chordlead」で、OpenALを利用した簡単な演奏機能をつけました。
が、シーケンス機能は完全に独自で実装してかなり苦労した部分で、もう少し楽にならないか?互換性を保ちたいと考えるようになりました。

MusicPlayer を使うメリットとして、以下の事項があげられると思います。

(1) SMF(Standard MIDI File)をサポートしている
(2) MusicSequence (シーケンサー)が利用できる
(3) AUGraph によるルーティングで内部音源が再生できる


ただし、MusicPlayer を利用するにしても AVAudioPlayer やSystem Sound を再生するのとは違い、下準備がそれなりに大変です。

利用する主なクラス


今回は MusicPlayer 以外に以下のクラスが頻繁に登場します。

(1) AudioUnit
オーディオを生成してアプリケーションに提供する重要なプラグインです。

(2) AUNode
オーディオは入力(マイク、音源)、エフェクト、出力と言った経路をたどって音が出ますが、AUNodeはそららを接続する単位です。

(3) AUGraph
AUNode をまとめたものが AUGraph です。
AUGraph 同士を接続することもできます。


抽象的ですが、音楽機材を用意してケーブルで繋ぎ合わせるようなイメージでしょうか?
と言う事で、実際にコーディングしてみました。

AVAudioSessionの設定


まず、AVAudioSession の設定と、ハードウェアのサンプリングレートを設定します。
- (BOOL) setupAudioSession
{
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    [audioSession setDelegate: self];

    NSError *audioSessionError = nil;
    [audioSession setCategory: AVAudioSessionCategoryPlayback error: &audioSessionError];
    if (audioSessionError != nil) {NSLog (@"Error setting audio session category."); return NO;}    

    _graphSampleRate = 44100.0;
    
    [audioSession setPreferredHardwareSampleRate: _graphSampleRate error: &audioSessionError];
    if (audioSessionError != nil) {NSLog (@"Error setting preferred hardware sample rate."); return NO;}
    
    [audioSession setActive: YES error: &audioSessionError];
    if (audioSessionError != nil) {NSLog (@"Error activating the audio session."); return NO;}

    _graphSampleRate = [audioSession currentHardwareSampleRate];
    
    return YES;
}
AVAudioSession setCategory では AVAudioSessionCategoryPlayback(iPhoneロック時でも再生) を設定し、サンプリングレートは 44.1kHz としました。
また、AVAudioSession では delegate を設定する事で、beginInterruption()、endInterruptionWithFlags () も利用する事ができます。

AUGraph、AUNode の作成


MusicPlayer を利用する前に、音源設定やオーディオのルーティングをしなければいけません。

まず、NewAUGraph で AUGraph を初期化します。
	OSStatus result = noErr;
	result = NewAUGraph(&_processingGraph);

次に、AudioComponentDescription で AUNode の入力側の設定詳細を設定します。
	AudioComponentDescription cd = {};
	cd.componentType = kAudioUnitType_MusicDevice;
	cd.componentSubType = kAudioUnitSubType_Sampler;
	cd.componentManufacturer = kAudioUnitManufacturer_Apple;
	cd.componentFlags = 0;
	cd.componentFlagsMask = 0;
        result = AUGraphAddNode(_processingGraph, &cd, &_samplerNode1);
componentType は、kAudioUnitType_MusicDevice(MIDI受信)componentSubType は kAudioUnitSubType_Sampler (内部音源 = サンプラー)を指します。
作成した AudioComponentDescription の情報を用いて、 AUGraph(_processingGraph) に 音源側の AUNode(_samplerNode1 )を追加します。

同様に、出力用の AUNode(_multiChannelMixerNode) を追加します。
	cd.componentType = kAudioUnitType_Output;
	cd.componentSubType = kAudioUnitSubType_RemoteIO;  
        result = AUGraphAddNode(_processingGraph, &cd, &_multiChannelMixerNode);

各AUNode に対して AUGraphNodeInfo() を用いて AudioUnit と関連づけます。
result = AUGraphOpen(_processingGraph);
result = AUGraphNodeInfo(_processingGraph, _samplerNode1, 0, &_samplerUnit1);
result = AUGraphNodeInfo(_processingGraph, _multiChannelMixerNode, 0, &_multiChannelMixerAudioUnit);

AUGraphConnectNodeInput() で出力と入力の AUNode を接続し、AUGraph を設定していきます。
result = AUGraphConnectNodeInput(_processingGraph, _samplerNode1, 0, _multiChannelMixerNode, 0);

AudioUnit の作成


各 AudioUnit の再生、入出力設定し、最終的に AUGraph をスタートします。
- (void) configureAndStartAudioProcessingGraph: (AUGraph) graph {
    OSStatus result = noErr;
    UInt32 framesPerSlice = 0;
    UInt32 framesPerSlicePropertySize = sizeof (framesPerSlice);
    UInt32 sampleRatePropertySize = sizeof (_graphSampleRate);

    //Sample rate
    result = AudioUnitSetProperty (_multiChannelMixerAudioUnit,
                                   kAudioUnitProperty_SampleRate,
                                   kAudioUnitScope_Output,
                                   0,
                                   &_graphSampleRate,
                                   sampleRatePropertySize
                                   );

    result = AudioUnitSetProperty (_samplerUnit1,
                                   kAudioUnitProperty_SampleRate,
                                   kAudioUnitScope_Output,
                                   0,
                                   &_graphSampleRate,
                                   sampleRatePropertySize
                                   );

    //Audio slice
    result = AudioUnitGetProperty (_multiChannelMixerAudioUnit,
                                   kAudioUnitProperty_MaximumFramesPerSlice,
                                   kAudioUnitScope_Global,
                                   0,
                                   &framesPerSlice,
                                   &framesPerSlicePropertySize
                                   );

    result = AudioUnitSetProperty (_samplerUnit1,
                                   kAudioUnitProperty_MaximumFramesPerSlice,
                                   kAudioUnitScope_Global,
                                   0,
                                   &framesPerSlice,
                                   framesPerSlicePropertySize
                                   );

    //AUGraph initialize
    if (graph) {
        //initialize AUGraph.
        result = AUGraphInitialize(graph);

        //Start AUGraph
        result = AUGraphStart(graph);
    }
}

AudioUnit でオーディオを扱う場合、AudioUnitSetProperty() で設定し、サンプリングレートやオーディオスライスの調整をしますが、定義は以下の通りです。
AudioUnitSetProperty(AudioUnit inUnit,
				  AudioUnitPropertyID		inID,
				  AudioUnitScope			inScope,
				  AudioUnitElement		inElement,
				  const void *			inData,
				  UInt32					inDataSize)
AudioUnitPropertyID は AudioUnit の種別
AudioUnitScope は、入出力関連の数値
AudioUnitElement は複数のBUS を利用する際に設定しますが、今回は1つだけなので 0です。
残りの引数は、データサイズを取得する為のものです。

音源楽器の設定


今回の楽器の音源方式は 「aupreset」ファイルを利用して、PCM音源でサウンド再生します。
aupreset
図のようにサンプリング音のパスは、 aupreset の URL で記述します。
ちなみに、このファイルを作らなくも MusicPlayer ではデフォルトで簡易音源(FM音源)が再生されます。

作成したaupreset ファイルを元に、AudioUnitSetProperty() で kAudioUnitProperty_ClassInfo で設定します。
- (OSStatus)loadSynthFromPresetURL: (NSURL *) presetURL
{
	CFDataRef propertyResourceData = 0;
	Boolean status;
	SInt32 errorCode = 0;
	OSStatus result = noErr;

	status = CFURLCreateDataAndPropertiesFromResource (
                                                       kCFAllocatorDefault,
                                                       (__bridge CFURLRef) presetURL,
                                                       &propertyResourceData,
                                                       NULL,
                                                       NULL,
                                                       &errorCode
                                                       );

	CFPropertyListRef presetPropertyList = 0;
	CFPropertyListFormat dataFormat = 0;
	CFErrorRef errorRef = 0;
	presetPropertyList = CFPropertyListCreateWithData (
                                                       kCFAllocatorDefault,
                                                       propertyResourceData,
                                                       kCFPropertyListImmutable,
                                                       &dataFormat,
                                                       &errorRef
                                                       );
    
	if (presetPropertyList != 0) {
		result = AudioUnitSetProperty(_samplerUnit1,
                                      kAudioUnitProperty_ClassInfo,
                                      kAudioUnitScope_Global,
                                      0,
                                      &presetPropertyList,
                                      sizeof(CFPropertyListRef)
                                      );
		CFRelease(presetPropertyList);
	}
    
    if (errorRef) CFRelease(errorRef);
	CFRelease(propertyResourceData);

	return result;
}

caf ファイル作成


サンプリング音のデータフォーマットは 今回 caf ファイルを利用しますが、OSX のターミナルでコマンドでコンバートする事ができます。

/usr/bin/afconvert -f caff -d aac piano_Bb.m4a piano_Bb.caf


ちなみにピアノのサンプリング音は、自宅のデジタルピアノから5音サンプリングしました。

MusicPlayer を使う


さて、ようやく本題です。
まず、MusicSequence のインスタンスを NewMusicSequence() で作成します。
   MusicSequence sequence = NULL;
    if (NewMusicSequence(&sequence) != noErr) NSLog(@"error");

次にリソース内の mid ファイルを MusicSequenceFileLoad() で読み込みます。
    NSString *soundName = [_songs objectAtIndex:songIndex];
    NSString *midiPath = [[NSBundle mainBundle] pathForResource:soundName ofType:@"mid"];
    inPathToMIDIFile = (__bridge CFURLRef)[NSURL fileURLWithPath:midiPath];
    result = MusicSequenceFileLoad(sequence, inPathToMIDIFile,
                                   kMusicSequenceFile_MIDIType,
                                   kMusicSequenceLoadSMF_ChannelsToTracks);
MusicSequenceFileTypeID は MIDIファイルなので kMusicSequenceFile_MIDIType
MusicSequenceLoadFlags は 現在 kMusicSequenceLoadSMF_ChannelsToTracks しかありません。

NewMusicPlayer() で MusicPlayer インスタンスを生成し、MusicPlayerSetSequence() で先ほど作成した MusicSequence を設定します。
    result = NewMusicPlayer(&musicPlayer);
    result = MusicPlayerSetSequence(musicPlayer, sequence);

最後にMusicSequence と AUGraph を接続し、シーケンサーを再生します。
    result = MusicSequenceSetAUGraph(sequence, _processingGraph);
    result = MusicPlayerStart(musicPlayer);

と、長々と実装してきましたが、再生はできるものの色々と問題がでてきました。

課題・問題点


(1) マルチチャンネルで楽器を再生できない
単にピアノ、ギターなどの単体楽器アプリなら今回のように問題なくMIDI再生できますが、現状バンド形式のように

複数の楽器を演奏する方法がわからず実装できませんでした


AUGraph で音源とBUS 設定でごにょごにょしてやれば可能かも知れませんが、そもそもMusicPlayer側でMIDIチャンネルをちゃんと認識してルーティングできるかが不明です。

(2) MIDI のコントロールチェンジ(CC)は反映されている?
今回、検証用として「HappyXmas」を自分で MIDI データ作ってみたのですが、PC(QuickTime)の再生 と iOS の再生が如実に違います。

特に CC64(サスティン)が効かず(?)ところどころブツブツ切れて不自然です


iOS上では何らかの理由で細かなニュアンスが再現できないようです。
設定が悪いのか?SMFファイルの作りが悪いのか?はたまたCC自体有効にならないのかが不明です。

という訳で、何とか「MusicPlayer」でクリスマスソングを再生することができました。

今回のサンプル


- Github
MusicTest
※最適化されてませんので途中で落ちる場合があります
※中のMIDIファイルは、あくまでも検証ファイルですのでご注意を

参考


- Music in iOS and Mac OS
- LoadPresetDemo
- iPhone Core Audioプログラミング

このサイトについて

HTML5 & CSS3化しつつあるので、現在IEには対応してません。
できれば、Google Chromeやら Apple SafariのWebKit系をお勧めします。

DBからプログラムまで一応全て自作なので、バグってたらすいません。
実験でFlash版(Flex版)を先に作りましたが、ちょっと停止してます。

プロフィール

新宿近辺でSE & プログラマーしてます。
Webアプリの開発・設計とか、最近はiPhoneとか奮闘してます。
デザインはさっぱりです。

音楽は、昔からCubase打ち込み人間で、そっちの方が経歴は長いですが、最近はやる暇がないです。。。

今は、Gon's Privates ってバンドのキーボードやってます。
単発的に、なんちゃってジャズ系のライブもやってます。

名古屋生まれなのでドラゴンズ好きです。

Info && SNS

Gmail

 yohei.yoshikawa@gmail.com

Twitter

 http://twitter.com/yoo_yoo_yoo

あんまつぶやきませんが、一応技術系メインで使ってます。情報交換はこちらへ

FaceBook

 http://www.facebook.com/#!/profile.php?id=1439130626

海外の知り合いがいないので閑散としてます。

mixi

 http://mixi.jp/show_profile.pl?id=230072

音楽仲間とかはこっちメインでやってます。興味があればこちらへ