MusicPlayer を使ってみる

2011/12/24

今回思い切って、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ファイルは、あくまでも検証ファイルですのでご注意を

参考