20201016のC#に関する記事は4件です。

WebAssembly でも ReactiveProperty を使ってみよう on Uno Platform

Uno Platform を使うと Windows, macOS, Linux, Android, iOS, WebAssembly 対応のアプリが作れます。
クロスプラットフォーム開発の苦しさはまぁあるとして、その苦しさとクロスプラットフォームに対応するメリットを天秤にかけて最終的に政治的な力によって採用・不採用が決まる感じですよね。

まぁそれは置いといて Uno Platform でも ReactiveProperty が使いたい!!ということで使うまでの手順を説明します。

やってみよう

ということでやっていきます。

プロジェクトの作成

Uno Platform のアプリのプロジェクトを作ります。

ReactiveProperty の参照の追加

NuGet で全プロジェクトに ReactiveProperty への参照を追加します。

Reactive.Wasm の追加

Wasm (Web Assembly) のプロジェクトに Reactive.Wasm を NuGet で追加します。そして Wasm のプロジェクトにある Program.cs の Main メソッドに EnableWasm の呼び出しを追加します。

using System;
using System.Reactive.PlatformServices;
using Windows.UI.Xaml;

namespace App10.Wasm
{
    public class Program
    {
        private static App _app;

        static int Main(string[] args)
        {
#pragma warning disable CS0618 // 型またはメンバーが旧型式です
            PlatformEnlightenmentProvider.Current.EnableWasm(); // これを追加
#pragma warning restore CS0618 // 型またはメンバーが旧型式です
            Windows.UI.Xaml.Application.Start(_ => _app = new App());

            return 0;
        }
    }
}

これで完了です。では使ってみましょう。

適当にテスト用コードの追加

ViewModel を Shared プロジェクトに追加します。

MainPageViewModel.cs
using Reactive.Bindings;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;

namespace App10.Shared
{
    public class MainPageViewModel
    {
        public ReactiveProperty<string> Input { get; }
        public ReadOnlyReactiveProperty<string> Output { get; }
        public AsyncReactiveCommand ResetCommand { get; }

        public MainPageViewModel()
        {
            Input = new ReactiveProperty<string>("");
            Output = Input.Select(x => x?.ToUpper() ?? "")
                .Delay(TimeSpan.FromSeconds(3))
                .ToReadOnlyReactiveProperty();

            ResetCommand = new AsyncReactiveCommand()
                .WithSubscribe(async () =>
                {
                    await Task.Delay(TimeSpan.FromSeconds(3));
                    Input.Value = "";
                });
        }
    }
}

ReactiveProperty でよくやる 3 秒後とかに大文字になるやつです。これを MainPage.xaml.cs のプロパティとして追加します。

MainPage.xaml.cs
using App10.Shared;
using Windows.UI.Xaml.Controls;

namespace App10
{
    public sealed partial class MainPage : Page
    {
        private MainPageViewModel ViewModel { get; } = new MainPageViewModel(); // add
        public MainPage()
        {
            this.InitializeComponent();
        }
    }
}

最後に XAML に適当にコントロールを足してバインドしましょう。

MainPage.xaml
<Page
    x:Class="App10.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:App10"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox Text="{x:Bind ViewModel.Input.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock Text="{x:Bind ViewModel.Output.Value, Mode=OneWay}" />
        <Button Command="{x:Bind ViewModel.ResetCommand}" Content="Reset" />
    </StackPanel>
</Page>

そして、Wasm のプロジェクトをスタートアッププロジェクトに設定して実行すると以下のような感じで動きます。WebAssembly でも ReactiveProperty が動きました!!

wasm.gif

念のため Android でも

and.gif

もちろん UWP でも

uwp.gif

iOS は試そうと思ってたら MacBook Pro を久しぶりに起動したら起動しなかった(!?)ので保留です。

まとめ

ということで Uno Platform を使うときも ReactiveProperty をぜひお願いします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Yamashitaの玩具箱

C#を中心に、なんとなくできてしまったものを公開中。
GitHubのリポジトリが散らかってきたのでハブ代わり(Hubのハブ?)にこのページを作成しました。

どの成果物も最低限必要な環境はWindows、VisualStudioだけ。

コンテンツ一覧

各GitHubリポジトリに飛んでいただくと使い方が書いてあります。


TransformationSample

画像の幾何学処理(拡大縮小回転射影)をいじって遊ぶアプリ。
tf2.png


YoloSamples

あの"YOLO"をC++やC#で使えるアプリ。学習から推論までオールインワンです。
csSample.png

ラベリングツールも同梱。
labellingtool.png


CPlusToSharpPractice

C++で作ったライブラリをC#で呼ぶ練習。内容は二次遅れ減衰振動のシミュレーション。
CppCsSample.png


PclSample

PointCloudLibraryという3D点群処理ライブラリのセットアップ。
pcl1.png
pcl2.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YAMASHITAの玩具箱

C#を中心に、なんとなくできてしまった副産物を公開中。
GitHubのリポジトリが散らかってきたのでハブ代わり(Hubのハブ?)にこのページを作成しました。

どの成果物も最低限必要な環境はWindows10、VisualStudio2019だけ。
何らかの助けになれば幸いです。

コンテンツ一覧

各GitHubリポジトリに飛んでいただくと使い方が書いてあります。


TransformationSample -- github

画像の幾何学処理(拡大縮小回転射影)をいじって遊ぶツール。
tf2.png


YoloSamples -- github

あの"YOLO"をC++やC#で使えるコード一式。学習から推論までオールインワンです。
csSample.png

ラベリングツールも同梱。
labellingtool.png


CPlusToSharpPractice -- github

C++で作ったライブラリをC#で呼ぶ練習。内容は二次遅れ減衰振動のシミュレーション。
CppCsSample.png


PclSample -- github

PointCloudLibraryという3D点群処理ライブラリのセットアップ。
pcl1.png
pcl2.png


FourierPractice -- github

フーリエ変換を画像に適用するサンプル。
スペクトルをいじって逆変換もできます。フィルタリングなどの参考になれば。
fourier4.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

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

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

image.png

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

AudioSourceとTextを作成します。
image.png

Objective-Cの中身を作成

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

こちらのサイトを参考にして私がエラー&バグを修正+加筆したソースを載せます。記事が8年前とかなり古めですがとても参考になりました。
(そのままではエラーが出るかもしれないですが今は放置で大丈夫です)

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;
    }


    /**************************************
     * ランダムで曲をエクスポートする
     * @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)に下記のソースをコピペして実行できるようにしましょう。

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
Main CameraにMediaController.csをアタッチ

ビルド!

これでもうUnityでの準備は完了なので、「PlayerSettings」を各自の環境に設定してビルドしましょう!!
(PlatformがiOSになってない方は先にiOSにSwitch Platformで切り替えてからビルドしてください)
image.png

実行!!

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

  • Info.plistにミュージックライブラリにアクセスする為の設定を追加
    1. 左にあるInfo.plistを選択して、「Information Property List」の右にある「+」を押す
    2. すると入力ボックスが現れるのでそこに「Privacy – Media Library Usage Description」を入力して右側に適当な文字列を入力します。

これで、アプリ起動時に端末のミュージックライブラリへアクセス許可を選択させるポップアップを表示させる事ができます。
image.png

Info.plistに設定を追加する

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

次は設定がちょっと厄介で画像のように、左の「Unity-iPhone」を押し真ん中上部付近の「Build Phases」を押して「Compile Sources」のボタンを押しましょう。
image.png

下にスクロールすると先ほど作成した「MusicLibraryMediaPicker.mm」があるのでダブルクリックして画像のように「-fno-objc-arc」と入力しましょう。
image.png

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

動かした様子

IMAGE ALT TEXT HERE

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

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

https://github.com/reishisu/ImportiOSMusicLibrary

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む