今回思い切って、iOS Advent Calendar2011 に参加してみました。 しかも最終日自分でいいのか?と思いましたが・・・最終回のネタは「MusicPlayer」です。 そして25日ということで、
クリスマスソングを再生させていただきます
MusicPlayer は OSX には既に存在していますが iOS5 から AudioToolbox.framework に追加された機能で、簡単に言うと音楽シーケンサーです。 ドキュメントを見る限り、OSX の機能はフルには使えないようですが、基本的な機能は利用できます。
個人的に作ったアプリ「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 の設定と、ハードウェアのサンプリングレートを設定します。
- (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 () も利用する事ができます。
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 の再生、入出力設定し、最終的に 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 の 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 ファイルを利用しますが、OSX のターミナルでコマンドでコンバートする事ができます。
/usr/bin/afconvert -f caff -d aac piano_Bb.m4a piano_Bb.caf
ちなみにピアノのサンプリング音は、自宅のデジタルピアノから5音サンプリングしました。
さて、ようやく本題です。 まず、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」でクリスマスソングを再生することができました。