【Unity】 iOS端末のミュージックライブラリから曲を取得して再生させる

UnityでiOS端末のミュージックライブラリの曲が必要になったのですが、参考になるサイトの情報が不足してエラーまみれになったり、記事自体が古かったので備忘録としてまとめます。

Objective-Cは、触って4日間の超初心者クオリティなので無駄なところがあるかもしれません。

AppleMusicの曲やDRMで保護がかかった曲は再生できないのでご了承ください。
(iTunesで購入した曲やCDからインポートした曲の再生は確認済み)

# 取得までの流れ

1. 直接はUnityからアクセス出来ないのでObjective-Cでミュージックライブラリにアクセス
2. Objective-Cでアクセスできても曲自体は直接取ることができないのでアプリのDocumentsフォルダ以下に曲をwav形式でエクスポート
3. DocumentsフォルダならUnityから直接アクセス出来るのでエクスポートしたwavファイルをWWWを用いてAudioClipに変換
4. 取得したAudioClipをAudioSourceにセットして再生！

今回はミュージックライブラリから1曲だけランダムで取得して再生するまでやっていきたいと思います。

# ミュージックライブラリにアクセスしてエクスポートするObjective-Cのファイルを準備
まずは、Unityのプロジェクトを作成して、「Asset」フォルダの中に「Plugins」フォルダを作成し、その中に「iOS」フォルダを作成、そしてその中に今回は「MusicLibraryMediaPicker.mm」と言う名前のファイルを作成します。
(.mmファイルは直接作成できないと思うので一旦外部のテキストエディタでファイルを作成してドラッグ＆ドロップしてインポートする方がいいと思います。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/a9900dc9-144f-9f36-f06e-8ef0a98e59c7.png)

# Objective-Cファイルを操作するC#のファイルを作成

そのままでは直接Objective-Cのファイルは操作できないので今回は操作する用のC#スクリプトを「Asset」フォルダ直下に「MediaController.cs」と言う名前で作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/4e204ec9-7a2f-bb53-ed7a-64cb741e1dcb.png)

# 曲を再生するAudioSourceと曲名を表示するTextを作成

AudioSourceとTextを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/b305cc83-7efd-374e-af30-495b09561f8b.png)

# Objective-Cの中身を作成

次はミュージックライブラリからアプリのDocumentsフォルダにエクスポートする処理を先ほど作成したObjective-Cのファイル(MusicLibraryMediaPicker.mm)に下記のソースをコピペしましょう。

[こちら](https://objective-audio.jp/2010/09/23/avassetreaderavassetwriter/)のサイトを参考にして私がエラー＆バグを修正+加筆したソースを載せます。記事が8年前とかなり古めですがとても参考になりました。
（そのままではエラーが出るかもしれないですが今は放置で大丈夫です）

```objc:MusicLibraryMediaPicker.mm
# import <Foundation/Foundation.h>
# import <MediaPlayer/MediaPlayer.h>
# import <AVFoundation/AVAudioFile.h>
# import <AVFoundation/AVAudioEngine.h>
# import <AVFoundation/AVFoundation.h>
# import <AVFoundation/AVAssetReader.h>
# import <AVFoundation/AVAssetWriter.h>

extern "C" {
    
    // プロパティ
    BOOL do_export;
    long song_id;
    NSString* song_name;
    
    // 関数のプロトタイプ宣言
    void exportRandomToItem();
    long getSongId();
    char* getSongName();
    BOOL getDoExport();
    
    
    /***************************************************
     * MPMediaItemをwav形式でDocumentフォルダに出力する関数
     * @param item 出力したい曲のMPMediaItem
     * @return 正しく出力できたらYESを返す
     ***************************************************/
    BOOL exportItem (MPMediaItem *item) {
        // エクスポートフラグを立てる
        do_export = YES;
        // エラー表示用の変数
        NSError *error = nil;
        // WAVEファイルのフォーマット
        NSDictionary *audioSetting = [NSDictionary dictionaryWithObjectsAndKeys:
                                      [NSNumber numberWithFloat:44100.0],AVSampleRateKey,
                                      [NSNumber numberWithInt:2],AVNumberOfChannelsKey,
                                      [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,
                                      [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
                                      [NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
                                      [NSNumber numberWithBool:0], AVLinearPCMIsBigEndianKey,
                                      [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
                                      [NSData data], AVChannelLayoutKey, nil];
        // 指定ファイルまでのパス
        NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
        // ↑の*urlからメディアデータへのアクセス用リンクを作成
        AVURLAsset *URLAsset = [AVURLAsset URLAssetWithURL:url options:nil];
        if (!URLAsset) {
            do_export = NO;
            return NO;
        }
        // ↑で作ったリンクをもとに指定されたアセットからメディアデータを読み取るアセットリーダーを返します。
        AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:URLAsset error:&error];
        if (error) {
            do_export = NO;
            return NO;
        }
        // メディアタイプのコンポジショントラックの配列を返す。
        NSArray *tracks = [URLAsset tracksWithMediaType:AVMediaTypeAudio];
        if (![tracks count]) {
            do_export = NO;
            return NO;
        }
        // アセットトラックからミックスされたオーディオデータを読み取る。
        AVAssetReaderAudioMixOutput *audioMixOutput = [AVAssetReaderAudioMixOutput
                                                       assetReaderAudioMixOutputWithAudioTracks:tracks
                                                       audioSettings:audioSetting];
        if (![assetReader canAddOutput:audioMixOutput]) {
            do_export = NO;
            return NO;
        }
        // 実際にミュージックデータを読み込む
        [assetReader addOutput:audioMixOutput];
        if (![assetReader startReading]) {
            do_export = NO;
            return NO;
        }
        // パスを作成
        NSArray *docDirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *docDir = [docDirs objectAtIndex:0];
        NSString *outPath = [[docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", [item valueForProperty:MPMediaItemPropertyPersistentID]]]
                             stringByAppendingPathExtension:@"wav"];
        // 書き込みファイルまでのパスまでのリンクを作成
        NSURL *outURL = [NSURL fileURLWithPath:outPath];
        // ↑で作ったリンクをもとに指定されたUTIで指定された形式で、指定されたURLで識別されるファイルに書き込むためのアセットライターを返します。
        AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outURL
                                                              fileType:AVFileTypeWAVE
                                                                 error:&error];
        if (error) {
            do_export = NO;
            return NO;
        }
        //ファイルが存在している場合は削除する
        NSFileManager *manager = [NSFileManager defaultManager];
        if([manager fileExistsAtPath:outPath]) [manager removeItemAtPath:outPath error:&error];
        if (error) {
            do_export = NO;
            return NO;
        }
        // データを書き込みする際に利用する
        AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
                                                                                  outputSettings:audioSetting];
        // リアルタイムで入力するか
        assetWriterInput.expectsMediaDataInRealTime = NO;
        if (![assetWriter canAddInput:assetWriterInput]) {
            do_export = NO;
            return NO;
        }
        // 書き込む情報を追加する
        [assetWriter addInput:assetWriterInput];
        if (![assetWriter startWriting]) {
            do_export = NO;
            return NO;
        }
        // コピー処理
        // ARCをオフにしているので自分で参照カウントを+1する
        [assetReader retain];
        [assetWriter retain];
        // 設定した情報を実際に書き込みを開始する
        [assetWriter startSessionAtSourceTime:kCMTimeZero];
        // 非同期処理
        dispatch_queue_t queue = dispatch_queue_create("assetWriterQueue", NULL);
        [assetWriterInput requestMediaDataWhenReadyOnQueue:queue usingBlock:^{
            while ( 1 ) {
                // ファイルの書き込みが出来るか
                if ([assetWriterInput isReadyForMoreMediaData]) {
                    // サンプルバッファーを出力用にコピーする
                    CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
                    if (sampleBuffer) {
                        // サンプルバッファーを追加する
                        [assetWriterInput appendSampleBuffer:sampleBuffer];
                        // オブジェクトを解放する
                        CFRelease(sampleBuffer);
                    } else {
                        // バッファーを追加出来ないようにする
                        [assetWriterInput markAsFinished];
                        break;
                    }
                }
            }
            // ディスパッチオブジェクトの参照（保持）カウントを減少させます。
            [assetWriter finishWriting];
            // ARCをオフにしているので自分で参照カウントを-1する
            [assetReader release];
            [assetWriter release];
            do_export = NO;
        }];
        dispatch_release(queue);
        return YES;
    }
    
    
    /**************************************
     * ランダムで曲をエクスポートする
     **************************************/
    void exportRandomToItem() {
        
        /// 曲情報を取得する処理
        MPMediaQuery* songQuery = [MPMediaQuery songsQuery];
        
        // 使える曲の配列
        NSMutableArray<MPMediaItem*>* array = [[NSMutableArray<MPMediaItem*> alloc] init];
        
        // ここでiCloudにしかない曲を弾く
        [songQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithBool:NO] forProperty:MPMediaItemPropertyIsCloudItem]];
        NSArray *songlists = songQuery.collections;
        
        // 使える曲リストを作成
        for ( int i = 0; i < [songlists count]; i++ ) {
            MPMediaItemCollection* songlist = [songlists objectAtIndex:i];
            MPMediaItem* item = [songlist representativeItem];
            if ( ![item hasProtectedAsset] ) [array addObject:item];
        }
        
        // 曲をエクスポート
        NSUInteger index = arc4random_uniform([array count]);
        MPMediaItem* item = [array objectAtIndex:index];
        song_id = [[item valueForProperty:MPMediaItemPropertyPersistentID] longValue];
        song_name = [item valueForProperty:MPMediaItemPropertyTitle];
        exportItem(item);
    }
    
    
    /************************************
     * セットされている曲のIDを取得する関数
     * @return セットされている曲のIDを返す
     ************************************/
    long getSongId() {
        return song_id;
    }
    
    
    /****************************************
     * セットされている曲のタイトルを取得する関数
     * @return セットされている曲のタイトルを返す
     ****************************************/
    char* getSongName() {
        return strdup([song_name UTF8String]);
    }
    
    
    /*******************************
     * コピー中かどうか判定する関数
     * @return コピー中ならYESを返す
     *******************************/
    BOOL getDoExport() {
        return do_export;
    }
}
```

このままではUnityから呼び出す事ができないので次はC#にObjective-Cと連携させる処理を作成します。

# C#の中身を作成

ここでは、Objective-Cのコードを実行させたいので先ほど作成したC#のファイル(MediaController.cs)に下記のソースをコピペして実行できるようにしましょう。

```c#:MusicLibraryMediaPicker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using UnityEngine.UI;
using System.IO;

public class MediaController : MonoBehaviour {

    private AudioSource audio;
    private Text text;

    #if UNITY_IOS
        [DllImport("__Internal")]
        public static extern void exportRandomToItem();

        [DllImport("__Internal")]
        public static extern long getSongId();

        [DllImport("__Internal")]
        public static extern string getSongName();

        [DllImport("__Internal")]
        public static extern bool getDoExport();
    #endif

    // Use this for initialization
	void Start () {

        // プロパティを取得
        audio = GameObject.Find("Audio Source").GetComponent<AudioSource>();
        text = GameObject.Find("Text").GetComponent<Text>();

        // ループ再生するようにする
        audio.loop = true;

        // コルーチンを開始
        StartCoroutine(MusicImport());
	}

    IEnumerator MusicImport() {

        text.text = "楽曲エクスポート中";

        // 曲エクスポートを開始
        exportRandomToItem();

        yield return new WaitForSeconds(0.25f);

        // 曲エクスポート完了まで待つ
        while ( getDoExport() ) yield return new WaitForSeconds(0.25f);

        text.text = "楽曲インポート中";

        // Documentsにある曲を取得
        string path = Application.persistentDataPath + "/" + getSongId() + ".wav";
        WWW www = new WWW("file://" + path);

        // インポートが完了するまで待つ
        while ( !www.isDone ) yield return new WaitForSeconds(0.25f);

        audio.clip = www.GetAudioClip(false, false);
	    
	text.text = "再生します！";

        audio.Play();

        text.text = getSongName();
	    
    	// wavファイルを削除
        System.IO.File.Delete(path);
    }
}
```

これでソースコードの準備は完了です！！

# C#とゲームオブジェクトを連携

先ほどObjective-Cを動かす為のC#ソースコードを作成しましたが、そのままでは動いてくれないのでUnityのオブジェクトにアタッチしてアプリ起動時に実行されるようにします。
なので今回は「Main Camera」のゲームオブジェクトにC#のスクリプトをアタッチしましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/cd7ce1a3-f404-be15-43b1-49a2ec3e866e.png)
Main CameraにMediaController.csをアタッチ

# ビルド！

これでもうUnityでの準備は完了なので、「PlayerSettings」を各自の環境に設定してビルドしましょう！！
(PlatformがiOSになってない方は先にiOSにSwitch Platformで切り替えてからビルドしてください)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/e4b0458d-51fb-f96e-ac65-497974f5cdb8.png)

# 実行！！

そのままでは実行できないので、いくつか設定する必要があります。

- Info.plistにミュージックライブラリにアクセスする為の設定を追加
  1. 左にあるInfo.plistを選択して、「Information Property List」の右にある「+」を押す
  2. すると入力ボックスが現れるのでそこに「Privacy – Media Library Usage Description」を入力して右側に適当な文字列を入力します。
 
これで、アプリ起動時に端末のミュージックライブラリへアクセス許可を選択させるポップアップを表示させる事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/19f45436-e15b-77f3-7ed3-a4aacd86201e.png)

Info.plistに設定を追加する

- 作成したObjective-CファイルのARCを無効にする

次は設定がちょっと厄介で画像のように、左の「Unity-iPhone」を押し真ん中上部付近の「Build Phases」を押して「Compile Sources」のボタンを押しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/fe8e08a5-7ad6-0608-36cf-ad6901d20a43.png)


下にスクロールすると先ほど作成した「MusicLibraryMediaPicker.mm」があるのでダブルクリックして画像のように「-fno-objc-arc」と入力しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241870/420c1eb6-3e66-071f-2275-586a651f6f01.png)

これで、準備は完了であとは自分の開発者アカウントでSigningして実行しましょう！！

# 動かした様子

[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/v05C3Fin55Q/0.jpg)](https://www.youtube.com/watch?v=v05C3Fin55Q)


# 作成したソースコード＆プロジェクト

作成したソースコードとプロジェクトは、GitHubにアップので良かったらどうぞ。

https://github.com/reishisu/ImportiOSMusicLibrary