仮想と物理とエトセトラ

xRや物理とかごった煮の備忘録的技術ブログ

簡易翻訳機を作ってみる その2 DeepLのWebAPIを使ってみる

今回は前回の続きです。

xr-physics-work-etc.hatenablog.com

DeepLのwebAPIを使用するための登録と、お試しで使用してみます。

「DEEPL API FREE」に登録する

まずは、DeepLのwebAPIを使用するために登録します。
今回は、試験的に利用するだけなので、月50万字制限のDeepL API Freeを使用します。
下記URLから「Sign Up for free」を選択します。

www.deepl.com

f:id:napo909:20210919143633p:plain

次にアカウントを作成するために、メールアドレスと、登録するパスワードを入力します。
f:id:napo909:20210919144147p:plain

次に住所とクレカの情報を登録します。
無料版ですが、不正利用を防ぐためにクレカ登録が必要なようです。
Pro版に手動でアップグレードしない限り、課金されることはないと記載されています。
言語はほぼ英語表記ですが、登録の際Last nameが先に来ていたり、県名が漢字で表記されていたりと日本人向けの登録フォームになっています。
f:id:napo909:20210919144442p:plain

登録すると、今回使用するサービス(DeepL API Free)についての概要が表示され、利用規約、翻訳が完全でない場合がある旨理解したことをチェックを入れます。
右下のSign Up for Freeを選択することでサインアップできます。
f:id:napo909:20210919145154p:plain

正しく利用規約などにチェックできていればアカウントの準備ができます。
f:id:napo909:20210919145448p:plain

作成されたDeepLのアカウント設定からは登録情報が確認できます。
Planからは契約しているプランの詳細と期間、Accountから登録情報とAPIを使用するための認証キー、Usageからは月50万字のうち、どの程度使用しているか確認することができます。
f:id:napo909:20210919145940p:plain

WebAPIを使った翻訳を試す

アカウントの作成ができたので、さっそくWebAPIを使った翻訳を試してみます。
DeepLのAPI仕様は下記に記載されています。

www.deepl.com

今回は手動で実施するため、curlコマンドで動作確認します。

APIの使用に必要なDeepLの認証キーは登録情報のAccountから下記項目を確認することで取得できます。
また、ログインしていると、API Reference中のExample中で認証キーは自動で補完されて表示されます。
f:id:napo909:20210919151121p:plain

APIのExampleを参考に下記コマンドを試します。

curl https://api-free.deepl.com/v2/translate auth_key=[DeepLの認証キー] -d "text=こんにちは、世界!" "target_lang=EN"

WindowsPowerShellの場合、パラメタ-dが複数あることに文句を言われるので、コマンドプロンプトで動作確認しました。

  • 1回目
    下記エラーが発生しました。
curl: (35) schannel: next InitializeSecurityContext failed: Unknown error (0x80092012) - 失効の関数は証明書の失効を確認 できませんでした。

調査したところ、SSL 証明書の失効チェックが実行できないようです。そのため、curl-kオプションを追加しました。

  • 2回目
    応答は帰ってきましたが、言語が英語と判断されてしまい、うまく翻訳できていません。

入力:

curl -k https://api-free.deepl.com/v2/translate auth_key=[DeepLの認証キー] -d "text=こんにちは、世界!" "target_lang=EN"

応答:

{"translations":[{"detected_source_language":"EN","text":"ɂ́AE!"}]}

応答が文字化けていることから、日本語送信時、文字コードが一致していないようです。
そのため、送る内容をjson化して、utf-8でファイルに保存したものを送る方法に変更しました。

コマンドプロンプトの場合、UTF-8文字コードで送るにはファイルを読み込ませるしかないようです。 zenn.dev そのため、"text=こんにちは、世界!"の部分のみファイル読み込みで再実施しました。

  • 3回目

入力:

curl -k https://api-free.deepl.com/v2/translate auth_key=[DeepLの認証キー] -d @text.txt "target_lang=EN"

※text.txtの内容:text=こんにちは、世界!
応答:

{"translations":[{"detected_source_language":"JA","text":"Hello, world!"}]}

無事入力言語が日本語と認識され、英語に翻訳することができました。
Windowsコマンドプロンプトの場合、curlで日本語を翻訳するのは不都合が多いので、アプリを作って運用するのがよさそうです。

なお、今回の動作確認で29文字ほど消費しました。
開発する分には月50万字を使い切ることはほぼないかな。
f:id:napo909:20210919161709p:plain

次回は、HoloLens2からDeepL APIを使用するための実装を行っていきます。

簡易翻訳機を作ってみる その1 方法検討および調査

最近小技だったり、SDKの使い方だったりが多いので、たまには小規模なアプリを作ってみようと思います。
今回は、HoloLensの音声入出力周りやweb APIをHoloLensから触る方法の学習も兼ねて、簡易翻訳機を作ってみようと思います。
※どこかで誰かがやっていそうな気はしますが、気にしない

翻訳後の文章はUnityEventなどに登録することでどこにでも出力できるようにして、様々なアプリに組み込むことができるようにしたいです。
そうすれば、HoloLensを用いたほかのアプリにも取り入れることができそうです。
せっかくMRTKを用いるので、Oculus Questなど別デバイスの対応もしたいですね。

内容の検討

今回の翻訳機はHoloLensを用いた以下の簡単な構成を検討しています。
追加で面白いものを思いついたら、別途追加しようと思います。
※画像認識と翻訳組み合わせられると面白そうですが、画像認識を結構ガチでやらないと精度でなさそう。

  1. HoloLensのマイクを用いて音声を入力する。
  2. 入力した音声を文章にする。
  3. 文章を翻訳する。
  4. 翻訳した結果を文章と音声として出力する。

技術検討

1, 2についてはHoloLensに標準搭載されているディクテーションの機能で実現できそうです。
ただし、ソフトウェアキーボードを毎度表示してディクテーションを開始するのはおっくうなので、ハンドメニューのボタンやボイスコマンドで翻訳開始、停止を実現したいです。

3についてはHoloLensのみでは難しいです。
外部のサービスに頼ることになりますが、翻訳精度の問題もあるので複数同時使用や、選択的に使用することを可能にしたいです。
現状である程度無料使用でき、HoloLensからweb APIをたたいて使用できるサービスは以下です。
ほかにもおもしろそう or 魅力的なものがあれば導入してみます。

  • Deepl www.deepl.com 月50万字まで無料(上限のため、課金される心配なし?)

  • Azure Translator azure.microsoft.com 毎月 200 万文字は無料

  • Google cloud.google.com 月に最初の50万字まで無料(それを超えると課金される模様)
    下記を使用するとタダでできそうだけど、規約的に大丈夫か? qiita.com

4についてはとりあえずはMRTK 2.7で追加されたTextToSpeechを用いてみようと思います。
docs.microsoft.com

ただし、どの程度の言語に対応しているか、現時点では不明なため、別のものも模索します。
未対応言語はとりあえず文字列出力ですかね。

※日本語の場合下記も使ってみたいですが、基本的には有料のためまたの機会に。

coefont.cloud

とりあえず、週によって作ったり作らなかったりするかとは思いますがのんびり作ってみようと思います。
短いですが、今回はここまで。

HoloLens2のファイルIOを確認する (その2 テキスト、画像、動画読み込み)

今回は前回の続きです。

xr-physics-work-etc.hatenablog.com

今回は、テキスト、画像、動画の読み込みを3種の方法で読み込んでみます。
読み込み方法は以下の通りです。

  • テキスト:WUPのFileIO.ReadTextAsyncFileStorageオブジェクトから読み込み
  • 画像:ファイルパスからSystem.IO.File.ReadAllBytesを用いたByte列読み込み
  • 動画:VideoPlayerコンポーネントでURL(ファイルパス)指定での読み込み

読み込み対象は、前回読み込めなかったMediaServerDevicesRemovableDevicesを除いた14種としました。

1. 準備

準備として読み込み用のコンポーネントとGameObjectを用意します。
読み込み用コンポーネントでは、プルダウンメニューから読み込み対象を選択し、対象ディレクトリのテキスト、画像、動画を読み込み対象とする構成です。

  • StorageReadCheck.cs
クリックで展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Video;
using TMPro;

#if WINDOWS_UWP
using System;
using Windows.Storage;
#endif

public class StorageReadCheck : MonoBehaviour
{
    /// <summary>
    /// ストレージ情報クラス
    /// </summary>
    [SerializeField]
    private StorageFolderInfo storageFolderInfo;
    
    /// <summary>
    /// タイトル
    /// </summary>
    [SerializeField]
    private TextMeshPro title;

    /// <summary>
    /// ビデオプレイヤー
    /// </summary>
    [SerializeField]
    private VideoPlayer videoPlayer;

    /// <summary>
    /// 画像表示用MeshRenderer
    /// </summary>
    [SerializeField]
    private MeshRenderer imageMeshRenderer;

    /// <summary>
    /// 文章読み込み用テキスト
    /// </summary>
    [SerializeField]
    private TextMeshPro text;



    // Start is called before the first frame update
    void Start()
    {
        LoadFiles();
    }

    // Update is called once per frame
    void Update()
    {

    }

    private void LoadFiles()
    {
        title.text = storageFolderInfo.targetStorageType.ToString();
        LoadVideo();
        LoadImage();
        LoadText();
    }

    private void LoadVideo()
    {
#if WINDOWS_UWP
        videoPlayer.url = storageFolderInfo.GetStorageFile("test.mp4").Path;
#endif
    }

    private void LoadImage()
    {
        Texture2D texture = new Texture2D(2, 2);
#if WINDOWS_UWP
        texture.LoadImage(System.IO.File.ReadAllBytes(storageFolderInfo.GetStorageFile("test.png").Path));
#endif
        imageMeshRenderer.material.mainTexture = texture;
    }

    private void LoadText()
    {
#if WINDOWS_UWP
        text.text = FileIO.ReadTextAsync(storageFolderInfo.GetStorageFile("test.txt")).GetAwaiter().GetResult();
#endif
    }
}

/// <summary>
/// ストレージ情報クラス
/// </summary>
[System.Serializable]
public class StorageFolderInfo
{
    
    public TargetStorageType targetStorageType;
#if WINDOWS_UWP
    public Dictionary<TargetStorageType, StorageFolder> targetStorageDict = new Dictionary<TargetStorageType, StorageFolder>();
#endif

    /// <summary>
    /// コンストラクタ
    /// ディクショナリ設定
    /// </summary>
    public StorageFolderInfo()
    {
#if WINDOWS_UWP
        targetStorageDict[TargetStorageType.AppCaptures] = KnownFolders.AppCaptures;
        targetStorageDict[TargetStorageType.CameraRoll] = KnownFolders.CameraRoll;
        targetStorageDict[TargetStorageType.DocumentsLibrary] = KnownFolders.DocumentsLibrary;
        targetStorageDict[TargetStorageType.MediaServerDevices] = KnownFolders.MediaServerDevices;
        targetStorageDict[TargetStorageType.MusicLibrary] = KnownFolders.MusicLibrary;
        targetStorageDict[TargetStorageType.Objects3D] = KnownFolders.Objects3D;
        targetStorageDict[TargetStorageType.PicturesLibrary] = KnownFolders.PicturesLibrary;
        targetStorageDict[TargetStorageType.Playlists] = KnownFolders.Playlists;
        targetStorageDict[TargetStorageType.RecordedCalls] = KnownFolders.RecordedCalls;
        targetStorageDict[TargetStorageType.RemovableDevices] = KnownFolders.RemovableDevices;
        targetStorageDict[TargetStorageType.SavedPictures] = KnownFolders.SavedPictures;
        targetStorageDict[TargetStorageType.VideosLibrary] = KnownFolders.VideosLibrary;
        targetStorageDict[TargetStorageType.LocalFolder] = ApplicationData.Current.LocalFolder;
        targetStorageDict[TargetStorageType.LocalCacheFolder] = ApplicationData.Current.LocalCacheFolder;
        targetStorageDict[TargetStorageType.RoamingFolder] = ApplicationData.Current.RoamingFolder;
        targetStorageDict[TargetStorageType.TemporaryFolder] = ApplicationData.Current.TemporaryFolder;
#endif
    }
#if WINDOWS_UWP
    /// <summary>
    /// フォルダ情報取得
    /// </summary>
    /// <returns></returns>
    public StorageFolder GetStorageFolder()
    {
        return targetStorageDict[targetStorageType];
    }
    /// <summary>
    /// ファイル情報取得
    /// </summary>
    /// <param name="fileName"></param>
    /// <returns></returns>
    public StorageFile GetStorageFile(string fileName)
    {
        return targetStorageDict[targetStorageType].GetFileAsync(fileName).GetAwaiter().GetResult();
    }
#endif
}

/// <summary>
/// 対象ストレージタイプ
/// </summary>
public enum TargetStorageType
{
    AppCaptures,

    CameraRoll,

    DocumentsLibrary,

    MediaServerDevices,

    MusicLibrary,

    Objects3D,

    PicturesLibrary,

    Playlists,

    RecordedCalls,

    RemovableDevices,

    SavedPictures,

    VideosLibrary,

    LocalFolder,

    LocalCacheFolder,

    RoamingFolder,

    TemporaryFolder,
}

次に、表示するためのオブジェクトを用意します。
タイトルとテキスト読み込み用にTextMeshPro, 動画再生、画像表示用にQuadを空のGameObjectの配下に作成します。
f:id:napo909:20210829145253p:plain

Quadのマテリアルには、アプリ実行中にテクスチャを更新できるようにAssigned at Runtimeにチェックを入れておきます。
f:id:napo909:20210829153952p:plain

動画再生用のQuadにはVidoPlayerコンポーネントをアタッチします。
f:id:napo909:20210829150047p:plain

先ほど作成したStorageReadCheckコンポーネントを親オブジェクトにアタッチして読み込み対象をプルダウンメニューから設定します。
f:id:napo909:20210829145743p:plain

当該オブジェクト(今回の場合FileLoadTest)をコピーし、それぞれ読み込みたい対象の読み込み対象をプルダウンメニューを変更して設定します。
最後に作成したFileLoadTestを空オブジェクトの配下に変更し、GridObjectCollectionコンポーネントで整列させます。
f:id:napo909:20210829150306p:plain f:id:napo909:20210829150322p:plain

2. 動作確認

ディレクトリにtest.png, test.mp4, test.txtを配置してアプリを起動すると読み込み結果が確認できます。
※作成対象のディレクトリの対応は前回を対象xr-physics-work-etc.hatenablog.com

実際に読み込みを行った結果は以下の通りです。
テキスト、画像はどこからでも読み込みができ、パス指定でUnity標準コンポーネントに読み込ませている動画のみ、アプリ用ディレクトリ中のフォルダからのみ読み込みができる結果となりました。

StorageFolder テキスト (UWP ReadTextAsync) 画像 (バイト列) 動画(ファイルパス読み込み)
KnownFolders.AppCaptures ○       ○       ×      
KnownFolders.CameraRoll ○       ○       ×      
KnownFolders.DocumentsLibrary ○       ○       ×      
KnownFolders.MusicLibrary ○       ○       ×      
KnownFolders.Objects3D ○       ○       ×      
KnownFolders.PicturesLibrary ○       ○       ×      
KnownFolders.Playlists ○       ○       ×      
KnownFolders.RecordedCalls ○       ○       ×      
KnownFolders.SavedPictures ○       ○       ×      
KnownFolders.VideosLibrary ○       ○       ×      
ApplicationData.Current.LocalFolder ○       ○       ○      
ApplicationData.Current.LocalCacheFolder ○       ○       ○      
ApplicationData.Current.RoamingFolder ○       ○       ○      
ApplicationData.Current.TemporaryFolder ○       ○       ○      

f:id:napo909:20210829150620j:plain

動画がアプリ固有ディレクトリ以外で読み込めない理由を確認してみます。
アプリ固有ディレクトリ中のTempState/UnityPlayer.logを確認すると対象ディレクトリ中のファイル読み込み時に以下のメッセージが出力されていました。
WindowsMediaFoundation received empty file U:\USERS\[ユーザ名]\[対象ディレクトリ]\test.mp4

Unity標準のVideoPlayerの場合はファイルパス読み込みのため、UWPの一般フォルダの厳しい読み込み権限の問題で、ファイルの内容が読み込めないのかもしれません。
今のところ、動画読み込みにVideoPlayerを用いる場合はアプリ固有のディレクトリに動画ファイルを置くしかありませんが、バイト列で動画を読み込めるようなアセットがあれば、他のテキストや画像と同じようにKnownFoldersからも読み込めるかもしれません。

HoloLens2のファイルIOを確認する (その1 フォルダ、ファイル作成)

今回は、自分自身の備忘録もかねて、HoloLens2のファイルIOを確認します。
2回ほどに分けて、フォルダ、ファイル作成と、動画や画像、テキストの読み込みを各フォルダから行えるかどうか確認します。

このあたりの情報は、HoloLens2出てすぐのものも多く、最新OSバージョンで挙動が変わっているものもあるかもしれないので、まとめてみます。

その1では、各フォルダにフォルダ、ファイル作成を行えるかどうか確認します。
対象は、KnownFoldersと、ApplicationData中のアプリ固有のフォルダです。

docs.microsoft.com

docs.microsoft.com

1. 準備

まずは、確認を行うための準備の内容です。
フォルダ、ファイル作成用のスクリプトを作成しました。
ファイル作成については、作成したファイルの読み込みも行っています。

  • StorageControlCheck.cs
クリックで展開
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

#if WINDOWS_UWP
using Windows.Storage;
#endif

public class StorageControlCheck : MonoBehaviour
{
    public async void EachStorageTest()
    {
        // https://docs.microsoft.com/ja-jp/uwp/api/windows.storage.knownfolders?view=winrt-19041
        // https://docs.microsoft.com/ja-jp/windows/uwp/get-started/fileio-learning-track
#if WINDOWS_UWP
        StorageTest(KnownFolders.AppCaptures);
        await Task.Delay(1000);
        StorageTest(KnownFolders.CameraRoll);
        await Task.Delay(1000);
        StorageTest(KnownFolders.DocumentsLibrary);
        await Task.Delay(1000);
        StorageTest(KnownFolders.MediaServerDevices);
        await Task.Delay(1000);
        StorageTest(KnownFolders.MusicLibrary);
        await Task.Delay(1000);
        StorageTest(KnownFolders.Objects3D);
        await Task.Delay(1000);
        StorageTest(KnownFolders.PicturesLibrary);
        await Task.Delay(1000);
        StorageTest(KnownFolders.Playlists);
        await Task.Delay(1000);
        StorageTest(KnownFolders.RecordedCalls);
        await Task.Delay(1000);
        StorageTest(KnownFolders.RemovableDevices);
        await Task.Delay(1000);
        StorageTest(KnownFolders.SavedPictures);
        await Task.Delay(1000);
        StorageTest(KnownFolders.VideosLibrary);
        await Task.Delay(1000);
        StorageTest(ApplicationData.Current.LocalFolder);
        await Task.Delay(1000);
        StorageTest(ApplicationData.Current.LocalCacheFolder);
        await Task.Delay(1000);
        StorageTest(ApplicationData.Current.RoamingFolder);
        await Task.Delay(1000);
        StorageTest(ApplicationData.Current.TemporaryFolder);
        await Task.Delay(1000);
#endif

    }

#if WINDOWS_UWP
    private async void StorageTest(StorageFolder storageFolder)
    {
        Debug.Log($"[{storageFolder.DisplayName}] storageFolder.Path:[{storageFolder.Path}]");
        Debug.Log($"MakeDir result about {storageFolder.DisplayName} is {await MakeDir(storageFolder, "test")}");
        Debug.Log($"MakeFile result about {storageFolder.DisplayName} is {await MakeFile(storageFolder, "test.txt")}");
    }
    
    
    private async Task<bool> MakeDir(StorageFolder storageFolder, string folderName)
    {
        try
        {
            StorageFolder targetDir = await storageFolder.CreateFolderAsync(folderName);
        
            Debug.Log($"[{storageFolder.DisplayName}] targetDir.Path:[{targetDir.Path}]");
            return true;
        }
        catch (System.Exception e)
        {
            
            Debug.Log($"[{storageFolder.DisplayName}] To make {storageFolder.Path + "/" + folderName}, raise exception {e}");
            return false;
        }
        
    }
    private async Task<bool> MakeFile(StorageFolder storageFolder, string fileName)
    {
        try
        {
            StorageFile targetFile = await storageFolder.CreateFileAsync(fileName);
            Debug.Log($"[{storageFolder.DisplayName}] targetDir.Path:[{targetFile.Path}]");
            await FileIO.WriteTextAsync(targetFile, "test");
            StorageFile readFile = await storageFolder.GetFileAsync(fileName);
            Debug.Log($"[{storageFolder.DisplayName}] read file contetns:{await FileIO.ReadTextAsync(readFile)}");
            
            return true;
        }
        catch (System.Exception e)
        {
            
            Debug.Log($"[{storageFolder.DisplayName}] To make {storageFolder.Path + "/" + fileName}, raise exception {e}");
            return false;
        }
        
    }
#endif
}

EachStorageTestメソッドをボタンから呼ぶことで、作成を実施します。
次に、Project SettingPlayerPublishing SettingCapabilitiesから各フォルダへのアクセス権を付与します。
今回付与した権限とその内容の対応は以下です。

  • MusicLibrary : Musicフォルダ配下
  • PicturesLibrary : Pictureフォルダ配下(カメラロールなど)
  • RemovableStorage : リムーバブルストレージへのアクセス
  • VideosLibrary : Videoフォルダ配下
  • Objects3D : Objects3Dフォルダ配下
  • RecordedCallsFolder : RecordedCallsフォルダ配下

なお、Documentsフォルダへの権限は、Unityから与えることはできません。
UnityでビルドしてVisualStudioソリューションファイルを作成した後、Package.appxmanifest </Capabilities>の前に<DeviceCapability Name="documentsLibrary" />を追加します。

  <Capabilities>
    <Capability Name="internetClient" />
    <uap:Capability Name="musicLibrary" />
    <uap:Capability Name="picturesLibrary" />
    <uap:Capability Name="removableStorage" />
    <uap:Capability Name="videosLibrary" />
    <uap:Capability Name="objects3D" />
    <uap2:Capability Name="spatialPerception" />
    <mobile:Capability Name="recordedCallsFolder" />
    <DeviceCapability Name="microphone" />
    <DeviceCapability Name="gazeinput" />
    <DeviceCapability Name="documentsLibrary" />    <===== 追加
  </Capabilities>

2. 結果

Windows Holographic for Business OSのバージョン21H1, OSビルド 20348.1014の時の結果は以下です。
今回作成したファイルはテキストファイルでしたが、Package.appxmanifestファイルの種類の関連付けは特に設定しなくても問題なくファイルの作成、読み込みができました。

StorageFolder 表示名取得 パス取得 ディレクトリ作成 ファイル作成 (ファイル読み込み含む) 補足
KnownFolders.AppCaptures キャプチャ U:\USERS\[ユーザ名]\Videos\Captures ○       ○      
KnownFolders.CameraRoll カメラ ロール U:\USERS\[ユーザ名]\Pictures\Camera Roll ×       ○       ディレクトリ作成のみ不可
KnownFolders.DocumentsLibrary ドキュメント 取得できず ○       ○       フォルダ、ファイル作成時のStorageFolder/Fileからはパス取得可。 U:\USERS\[ユーザ名]\Documents\
KnownFolders.MediaServerDevices Media Servers 取得できず ×       ×       Unspecified error。対象が未接続のためと思われる。
KnownFolders.MusicLibrary ミュージック 取得できず ○       ○       フォルダ、ファイル作成時のStorageFolder/Fileからはパス取得可。 U:\USERS\[ユーザ名]\Music\
KnownFolders.Objects3D 3D オブジェクト U:\USERS\[ユーザ名]\3D Objects ○       ○      
KnownFolders.PicturesLibrary ピクチャ 取得できず ○       ○       フォルダ、ファイル作成時のStorageFolder/Fileからはパス取得可。 U:\USERS\[ユーザ名]\Pictures\
KnownFolders.Playlists プレイリスト U:\USERS\[ユーザ名]\Music\Playlists ○       ○      
KnownFolders.RecordedCalls 録音した通話 U:\USERS\[ユーザ名]\Recorded Calls ○       ○      
KnownFolders.RemovableDevices Removable Storage Devices 取得できず ×       ×       Unspecified error。対象が未接続のためと思われる。
KnownFolders.SavedPictures 保存済みの写真 U:\USERS\[ユーザ名]\Pictures\Saved Pictures ○       ○      
KnownFolders.VideosLibrary ビデオ 取得できず ○       ○       フォルダ、ファイル作成時のStorageFolder/Fileからはパス取得可。 U:\USERS\[ユーザ名]\Videos\
ApplicationData.Current.LocalFolder LocalState U:\Users\[ユーザ名]\AppData\Local\Packages\[アプリフォルダ]\LocalState ○       ○      
ApplicationData.Current.LocalCacheFolder LocalCache U:\Users\[ユーザ名]\AppData\Local\Packages\[アプリフォルダ]\LocalCache ○       ○      
ApplicationData.Current.RoamingFolder RoamingState U:\Users\[ユーザ名]\AppData\Local\Packages\[アプリフォルダ]\RoamingState ○       ○      
ApplicationData.Current.TemporaryFolder TempState U:\Users\[ユーザ名]\AppData\Local\Packages\[アプリフォルダ]\TempState ○       ○      

カメラロールのみ、ファイルの作成はできましたが、フォルダの作成はできませんでした。
ほかの多くのフォルダについては、ディレクトリ、ファイルの作成ができることが確認できました。
フォルダに対して権限を与えれば、アプリからファイル、フォルダのやり取りが比較的容易にできることがわかりました。

次回は、実際にUnity上で動画や画像、テキストの読み込みを比較していきます。
※ 今回初めて表を使ってみましたが、なんか見づらいですね。。。どうにかできないものか。。。

iOSアプリデプロイがうまくいかない場合の原因確認方法 ([端末名] is not available. Please reconnect the device.)

今回はMRTKとAR FoundationでiOSアプリを作成し動作確認する、をネタにする予定でしたがうまくいかなかったので原因確認の過程を残しておきます。
ビルドの準備から原因確認の過程のネタなので、Macの端末でUnityエディタでXcode用のプロジェクトを作成するまでの過程は割愛します。

ビルド実施まで

Unity上でビルドすると下記のようなファイル群がビルド時に指定したフォルダに作成されます。 フォルダ中の.xcodeprojファイルをダブルクリックすると、Xcodeが開かれます。
f:id:napo909:20210815172616p:plain

プロジェクトを開いた後、Xcodeビルドを行う前に設定する事項があります。
iOSアプリをビルドするには、アプリに署名が必要です。
XcodeSigning & Capabilitiesタブを選択するとステータスがエラーになっていると思います。
f:id:napo909:20210815165229p:plain

すでにApple IDをXcodeに登録済みの場合はTeamのプルダウンメニューから自分のアカウントを選択します。
登録していない場合は、Add an Account...からアカウントを登録します。
f:id:napo909:20210815165523p:plain

なお、ベータ版のリリースや、App StoreにアプリをリリースするにはApple Developer Programに入る必要があります。

developer.apple.com

無事Teamの登録ができると、出ていた警告が消えます。
※消えない場合は自分のアカウントに紐づいている権限が、Build SettingsSigningに登録されているものと異なる可能性があります。
f:id:napo909:20210815165704p:plain

次にデプロイする端末を接続します。
LightningケーブルまたはUSB-CケーブルをMacに接続します。
端末を接続した後、Generic iOS Deviceを選択すると、接続した端末を選択できるようになります。
この端末が、ビルド後のデプロイ先となる端末です。
f:id:napo909:20210815170114p:plain

デプロイ先の選択が終わったらビルドボタン(右向き三角)を押しビルドとデプロイを行います。
今回、ビルドはすんなり終わりましたが、端末が有効でなく再接続を求められるメッセージが表示されました。
ケーブルの再接続や、PCの再起動を試しましたが状況は変わりませんでした。
f:id:napo909:20210815170238p:plain

原因の確認

Xcodeでは、接続した端末の情報を見ることができます。
上部のメニューからWindowsDevice and Simulatorsを選択します。
f:id:napo909:20210815170414p:plain

選択後出てきたウィンドウから接続しているデバイスの状態を確認できます。
今回はいくつかエラーが出ていました。
エラーの内容としては端末のiOS 14.7に対し、Xcodeのバージョンが古いためサポートしていないとのことでした。
f:id:napo909:20210815170729p:plain

インストール済みのXcodeのバージョンは11.4.1、対して最新バージョンは12.5.1と確かにメジャーバージョンが1古いのが原因のようです。
最新バージョンのXcodemacOS 11.0以降(Big Sur)が必要ですが、私の保有しているMac Book Air (11-inch Mid 2012)では対応していません。
残念ながら、新しいMac OSのPCを調達しないと、iOSの開発はできなさそうです。
一応、非対応端末にBig Surをインストールする方法もあるようですが、リソース的に無理にインストールするため、開発に使用できるかというと微妙です。

myzwtk.hatenablog.com

毎年、9月以降に新型が出ているので、Apple Silicon搭載のMacを新しく調達しようかな。。。

japanese.engadget.com

HoloLens2用アプリに再ビルド不要なデバッグモードを追加する(プロトコルでの実装)

今回は小技です。
前回、プロトコルから設定することでHoloLens2用アプリケーションでも起動時のモードを切り替えることができることがわかりました。

xr-physics-work-etc.hatenablog.com

今回はこのプロトコルで設定できるURIスキームを用いて、通常起動では実行されないデバッグモードを用意してみます。

URIスキームを用いたアプリケーションランチャーを作る

前回の記事で、新しいHoloLens2のOSではブラウザからURIスキーマを用いたアプリケーションの起動が行えませんでした。
そのため、まずはURIスキームを用いてアプリケーションを起動できる簡単なHoloLens2アプリケーションを作成します。

今回はシンプルにMRTK導入後、InputFiledとボタンだけで構成しました。 f:id:napo909:20210809161727p:plain

次にURIからアプリケーションを起動するためのスクリプトを作成します。

  • LaunchApp.cs
クリックで展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.WSA;

using TMPro;

public class LaunchApp : MonoBehaviour
{
    /// <summary>
    /// 実行するURIスキーム
    /// </summary>
    private string uriScheme = "";
    
    /// <summary>
    /// 入力対象のInput field
    /// </summary>
    [SerializeField]
    private TMP_InputField inputFiled;

    /// <summary>
    /// URIスキームを設定する関数
    /// </summary>
    public void SetScheme()
    {
        uriScheme = inputFiled.text;
    }

    /// <summary>
    /// URIスキームを元に実行する
    /// </summary>
    public void LaunchApplication()
    {
        try
        {
            Debug.Log($"Launch {uriScheme}");
            Launcher.LaunchUri(uriScheme, true);
        }
        catch (System.Exception e)
        {
            Debug.Log(e);
        }
    }
    
}

作成したスクリプトコンポーネントとしてアタッチし、コンポーネントにInputFieldを参照させます。
f:id:napo909:20210809162149p:plain

また、InputFiledにURIスキームの設定(SetScheme)を、ボタンにURIスキームを元としたアプリケーションの実行(LaunchApplication)を設定します。
f:id:napo909:20210809181016p:plain

f:id:napo909:20210809163036p:plain

これで、InputFieldに入力したURIからアプリケーションを起動できるようになりました。

プロトコルデバッグモードを実装する

次に、デバッグモード仕込むアプリケーションを作成します。
今回はアプリ名をURISchemeTestにしています。
このアプリケーションがアプリケーションランチャーから起動されるものです。
MRTKを導入後、以下のようなHierarchyでシーンを設定します。
デバッグモードでない場合にも何か表示させるために、Cubeも配置しています。
f:id:napo909:20210809173254p:plain

デバッグモード用に、デバッグモードである旨を示すテキストとして、Debug Mode(TextMeshPro)、デバッグログ表示のためLogger(TextMeshPro)を用意しています。
今回は、Logger(TextMeshPro)でのログ出力にホロモンさんの下記を使用しました。

bluebirdofoz.hatenablog.com

次に、URIを解釈し、必要があればデバッグモードに遷移するためのスクリプトを作成します。
今回は?区切りでDebugの文字列がURI中にある場合はデバッグモードとして扱うようにしました。

  • DebugControlManager.cs
クリックで展開
using UnityEngine;
using Microsoft.MixedReality.Toolkit;
public class DebugControlManager : MonoBehaviour
{
    /// <summary>
    /// 自インスタンス
    /// </summary>
    private static DebugControlManager instance;

    /// <summary>
    /// デバッグモードでのみ表示するオブジェクト
    /// </summary>
    [SerializeField]
    private GameObject[] targetObjects;

    /// <summary>
    /// デバッグモードかどうか
    /// </summary>    
    public bool isDebugMode = false;

    /// <summary>
    /// デバッグモードにする場合のコマンドキーワード
    /// </summary>
    private static string debugKeyword = "Debug";

    void Awake()
    {
        // シングルトン化
        if (instance == null)
        {
            // 未作成の場合作成
            instance = this;
            DontDestroyOnLoad(this.gameObject);
        }
        else
        {
            // すでに作成されている場合削除
            Destroy(this.gameObject);
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Start Check URI Schema");
        Debug.Log($"URI:{Application.absoluteURL}");

        // カスタム URI スキームを使用してアプリケーションが起動されている場合、
        // Application.absoluteURLに値が入る
        if (!string.IsNullOrEmpty(Application.absoluteURL))
        {

            GetURIInfo(Application.absoluteURL);
        }
        SetDebugState();
    }

    // Update is called once per frame
    void Update()
    {

    }

    /// <summary>
    /// URI情報取得処理
    /// </summary>
    /// <param name="uri"></param>
    void GetURIInfo(string uri)
    {
        string[] command;

        try
        {
            command = uri.Split('?');
        }
        catch (System.Exception e)
        {
            Debug.Log(e);
            return;
        }

        DecodeURICommand(command);


    }

    /// <summary>
    /// URIコマンド解釈処理
    /// </summary>
    /// <param name="args"></param>
    void DecodeURICommand(string[] args)
    {
        isDebugMode = false;

        foreach (var arg in args)
        {
            if (arg == debugKeyword)
            {
                Debug.Log("Exists debug command");
                isDebugMode = true;
            }
        }

    }

    /// <summary>
    /// デバッグ状態の設定処理
    /// </summary>
    void SetDebugState()
    {
        foreach (GameObject targetObject in targetObjects)
        {
            // 対象オブジェクトの有効/無効化
            targetObject.SetActive(isDebugMode);
        }

        // MRTKの診断の有効/無効化
        MixedRealityToolkit.DiagnosticsSystem.ShowDiagnostics = isDebugMode;


    }
}

作成したDebugControlManagerを適当なオブジェクトにアタッチして、デバッグモードでのみ表示したいオブジェクトを設定します。
f:id:napo909:20210809175552p:plain

次に、URIスキームを使用するために必要な、プロトコルの設定を行います。
PlayerSettingPlayerPublishing SettingsのProtocolにURIスキーマとして設定するためのキーワードを設定します。
今回はuri-scheme-testを設定しました。
f:id:napo909:20210809171138p:plain

これでURIスキームを使用するための設定ができました。
UnityアプリケーションをUWP用にビルドしてできたVisual StudioのソリューションファイルからPackage.appxmanifestを確認すると、プロトコルの設定が正しくされていることを確認できます。
f:id:napo909:20210809175716p:plain

動作確認

動作確認として3パターン試してみます。

1. いつもアプリケーションを実行する場合と同じようにURISchemeTestを実行する

こちらはメニューからのいつも通りの起動のため、結果のみ記載すると、

[結果]

  • 赤いCubeのみ表示される。
  • MRTK標準の診断は表示されない。

f:id:napo909:20210809174445j:plain

2. アプリケーションランチャーから起動する

今回設定したURIスキームをアプリケーションランチャーに設定して起動します。
特に設定値はないので、適当に入力します。
uri-scheme-test://hoge?hoge

[結果]

  • URIスキームに対応するアプリケーション(今回の場合はURISchemeTest)が実行される
  • デバッグオプションを付与していないので、それ以外は1.と同じ

f:id:napo909:20210809174535j:plainf:id:napo909:20210809174543j:plain

3. URIデバッグオプションを加えてアプリケーションランチャーから起動する

次にURIデバッグオプションを追加して実行します。
uri-scheme-test://hoge?hoge?Debug

[結果]

  • URIスキームに対応するアプリケーション(今回の場合はURISchemeTest)が実行される
  • デバッグモードでのみ表示設定したDebug Modeの文字列とデバッグログが表示される
  • (スクリーンショットに映っていないが)MRTKの診断が表示される

f:id:napo909:20210809174640j:plainf:id:napo909:20210809174651j:plain

これで、プロトコルURIスキーム)を用いたデバッグモードを仕込むことができました。
再ビルドしなくても、通常起動と、デバッグモードをHoloLens2のみで切り替えることができます。
他にも、アプリケーションへの引数の渡し方として応用できるような気がします。

Holographic Remotingでクライアント(HoloLens2)からサーバ(PC)に向けて接続する

今回はHolographic Remotingの小技をメモします。

Holographic Remotingを使用する場面として、UnityのHolographic Emulationや以前記事にしたUnityアプリケーションでのHolographic Remotingリモートアプリを作成する場合があります。

xr-physics-work-etc.hatenablog.com

これらの場合、どちらもPC側からHoloLens2に接続を行います。
HoloLens2側のアプリである、Holographic Remoting Playerも、普通にアプリを起動すると、接続を待ち受けるモード(Listen)で起動されます。

しかし、下記説明を見るとHoloLens2からPCに接続しに行くこと(Connect)もできるようです。

docs.microsoft.com

今回はHoloLens2からPCに接続しに行く方法についてメモします。

動作確認準備

1. サンプルを取得する

今回は、リモート側アプリとして、Holographic Remotingの公式サンプルを使用します。

github.com

gitを用いて、リポジトリをcloneするか、Download ZIPを選択しZIPファイルをダウンロードします。

f:id:napo909:20210801113331p:plain

HoloLens2側で使用するHolographic Remoting Playerは、公式サンプルのものとMicrosoft Storeにあるものとはほぼ同じため、Microsoft Storeものを使用します。

www.microsoft.com

2. サンプルアプリをビルドする

今回、リモート側アプリはdesktopを使用します。

github.com

MixedReality-HolographicRemoting-Samples/remote/desktop/中のSampleRemote.slnをダブルクリックします。

ビルドの設定をRelease x64に変更します。

f:id:napo909:20210801114416p:plain

上部メニューからビルドデバッグなしで開始を選択しビルドを行います。

f:id:napo909:20210801114510p:plain

なお、私の環境では下記エラーが発生しました。

f:id:napo909:20210801114658p:plain

ソリューションのプロパティからSpectre軽減策を無効化することで解消しました。

f:id:napo909:20210801115106p:plain

ビルドに成功すると、\MixedReality-HolographicRemoting-Samples\remote\desktop\bin\ReleaseSampleRemote.exeが生成されています。
これが、Holographic Remotingのリモート側アプリです。

3. HoloLens2→PCの接続方法を確認する

リモートアプリについて、ソリューションからSampleRemoteApp.cの中身を確認すると、コマンドライン引数から様々な設定ができることがわかります。

github.com

L216から、-listenコマンドライン引数につけるとリモートアプリがHoloLens2からの受信待ちの状態で、アプリが起動することがわかります。

同様にHolographic Remoting Playerアプリ側も確認します。
HolographicRemoting\MixedReality-HolographicRemoting-Samples\player\sampleSamplePlayerMain.cppを確認すると、特にコマンドライン引数などで設定されていない場合は、m_playerOptionsに格納されているデフォルトの設定が適用され、Listenモードで起動します。

HoloLens2の場合は、コマンドライン引数で指定することは(私の知る限りは)できません。
SamplePlayerMain.cppのL650を確認すると、プロトコルを使用して設定できるようです。
HolographicRemoting\MixedReality-HolographicRemoting-Samples\player\samplePackage.appxmanifestから宣言の項目を確認すると、プロトコルの設定がされています。

f:id:napo909:20210801125456p:plain

プロトコルではURIスキームを宣言できます。

www.atmarkit.co.jp

そのため、URIスキームを使用してHolographic Remoting Playerを起動することで、引数を与えた状態で起動をすることができます。

動作確認

まず、リモートアプリを引数を与えて実行します。
Windows Powershell\MixedReality-HolographicRemoting-Samples\remote\desktop\bin\Releaseに移動し、下記のコマンドでリモートアプリを実行します。
.\SampleRemote.exe -listen
特にポート番号を指定していないので、待ち受けているポートは8265です。

実行するとウィンドウが表示されます。
タイトルバーを見るとPress Space To Connectと表示されており、スペースキーを押すと接続が開始されることがわかります。
スペースキーを押下すると、接続対象のIPアドレスが表示されますが、0.0.0.0で表示されているためListen状態で接続待ちしていることがわかります。
f:id:napo909:20210801130328p:plain

そのあと、PC側のIPアドレスをipconfigなどで確認しておきます。

次にHoloLens2側でブラウザを開き、URLを入力する欄に下記URIを設定します。
ms-holographic-remoting://[PCのIPアドレス]:8265

f:id:napo909:20210801135103j:plain

※どうやら2021年8月1日現在、最新のWindowsHolographic バージョン 21H1で使用できるCromium版のEdgeブラウザでは、ブラウザからURIスキームを使用してのアプリ起動ができないようです(私のやり方が悪いだけ?)。この場合は、別途HoloLens2用アプリに組み込んで実行する必要があります。

docs.microsoft.com

IPアドレスが正しく設定されていれば、Holographic Remoting Playerが起動され、PC側のリモート側アプリに接続されます。

f:id:napo909:20210801135205j:plain

通常の方法でHolographic Remoting Playerを起動すると①のような表示になりますが、今回の方法で起動すると②のようになり、HoloLens2側から接続しに行っていることがわかります。
① Waiting for connection on f:id:napo909:20210801135659j:plain

② Connecting to f:id:napo909:20210801135709j:plain

これでHoloLens2からPCに接続する方法でHolographicRemoting接続することができました。
なお、Unityから触れるHolographic RemotingのAPIではリモートアプリから接続しに行くAPIしかないようです。

docs.unity3d.com

UnityからもListenの設定ができればよりHolographic Remotingの用途が広がるような気がします。
何か方法はないものか。。。