NrealLight

(3ヶ月前にメモ書きしていた記事です。)

概要

中国のスタートアップNreal(エンリアル)社が開発したMRグラス。

www.nreal.ai

メガネ型、88g、4基のカメラで空間認識、視野角52度(Oculus Quest*1は100度)、解像度1920×1080(非公表数値; Oculus Questは1600x1440)。 SLAM(Simultaneous Localization and Mapping; 自己位置推定と環境地図作成)、平面認識、画像認識を搭載。 現実世界のオブジェクトを解析してCGを現実世界に反映させる高度なMR機能は対応していないらしい。

Snapdragon 855を搭載したAndroidスマートフォンとUSB Type-Cで有線接続して使用。以下は、Snapdragon 855搭載Androidスマートフォン:

アプリケーションは以下が動作可能になる:

主な出来事

時期 出来事
2019年5月 KDDIとの提携を発表
2019年12月9日 KDDIとNreal社はMRグラス「NrealLight」を用いた開発プログラム「EVE2020」を開始
2019年9月 開発者版提供開始(1199ドル)
2020年初旬 一般発売予定(499ドル)

NRSDK 1.0 Beta

Unityのみ対応(いずれは Unreal Engine, Android Native にも対応)

空間コンピューティング(Spatial Computing)

  • 6DoFのトラッキング(6DoF Tracking)
  • 平面認識(Plane Detection)
    • 1.0 Beta では水平のみ
  • 画像認識(Image Tracking)
  • 環境のマッピング
  • 視界内の対象物を分析、認識、理解

レンダリングの最適化(Rendering Optimization)

Multi-modal Interactions

  • Nreal Light Controller (3DoF)
  • Nreal Phone Controller (3DoF)
    • 未サポート

Developer Tools

サードパーティSDK拡張 (3rd Party SDK Extension)

  • センサー(例:RGBカメラ)からデータにアクセスする、など

参考文献

*1:Oculus QuestはVRバイスであり、MRデバイスであるNrealLightとの比較は不適切かもしれませんが、比較的手頃に入手できるデバイスとの比較ということで。私が所持しているというということが一番の理由ですが。

Chromeで範囲選択したテキストをDeepL翻訳で翻訳する

概要

Google翻訳を超える翻訳の精度と噂されるDeepL翻訳を使う際、わざわざDeepL翻訳のページを開いてテキストを入力するのは手間がかかります。 Google翻訳には選択範囲をGoogle翻訳で翻訳するChrome拡張機能がありますが、DeepL翻訳はまだ無いようです。 しかし、Chrome拡張機能「Selection Search」を使えば範囲選択した範囲を翻訳させることができます。 この記事では、「Selection Search」を使用してDeepL翻訳を使用する方法を説明します。

目次

確認環境

解説

目指している状態は次の画像のとおりです。テキストを範囲選択し、メニューで en -> ja (英語から日本語に翻訳) 、ja -> en (日本語から英語に翻訳)を選択するとDeepLのページが開き翻訳が行われます。

まず、Chrome 拡張機能 Selection Search を追加します。

chrome.google.com

次に、Selection Search のオプションを変更します。オプションは次の画像のメニューから開けます。アドレスバーに chrome-extension://gipnlpdeieaidmmeaichnddnmjmcakoe/options/options.html を貼り付けてもオプションが開けます。

オプション項目の Search engines を編集します。#en/ja/#ja/en/ で翻訳元言語と翻訳先言語を指定しています。

ここまででも構いませんが、お好みに合わせて以下の設定を変えるとよいでしょう。

Popup Menu を Auto にするとテキストを範囲選択するだけでメニューが開きます。範囲選択するだけでメニューが開くと邪魔な場合は、Mouse Click などを選択すると良いでしょう。

Other Options には Open search in new tab (新しいタブで検索結果を開く)設定があります。他にも設定があるので、色々と試しつつお好みの設定を探してみると良いと思います。

Android Studio のログにソースコードへのリンクを表示する

概要

Android Studio のログ出力にソースコードへのリンクを表示する方法です。

目次

確認環境

  • AndroidStudio 3.5.2
  • Kotlin 1.3.50

参考情報

解説

Android Studio のログ出力では、クラス名.メソッド名(ファイル名:行数) の形式で表示したときにソースコードへのリンクが有効になります。 Logcat, Instrumentation Test の実行ログでリンクが有効になることを確認しています。

fun printLinkToCode(tag: String) {
    val clazz = object {}::class.java
    val enclosingClassName = requireNotNull(clazz.enclosingClass?.name)
    val enclosingMethodName = requireNotNull(clazz.enclosingMethod?.name)

    val stackTrace = Thread.currentThread().stackTrace
    val indexOfThisMethod = stackTrace.indexOfFirst {
        it.className == enclosingClassName && it.methodName == enclosingMethodName
    }

    val caller = stackTrace[indexOfThisMethod + 1]
    Log.i(tag, "${caller.className}.${caller.methodName}(${caller.fileName}:${caller.lineNumber})")
}

Zero Caliber VR を Oculus Quest でプレイする

概要

SteamVR で配信されている Zero Caliber VR を Qculus Quest でプレイする方法です。Virtual Desktop を使用しています。

目次

確認環境

  • Virtual Desktop 1.6.2 (SIdeloading)
  • Zero Caliber VR (2019/10/14 時点のバージョン)

解説

SteamVR のアプリを Oculus Quest で実行する方法については、他の記事を参照してください。Zero Caliber VR を Oculus Quest で起動できた前提で話しを進めます。

2019/10/14 現在、Zero Caliber VR を Oculus Quest で実行すると Oculus Touch の操作を何も受け付けてくれません。この問題は SteamVR の Controller Settings から設定を行うことで解決できます。

Current Binding の Edit を選択すると Zero Caliber VR の Action を設定できます。少々面倒ですが、各ボタンに Action を割り当てます。私は下記の割り当てでチュートリアルまでクリアできました。

  • Left Oculus Touch
    • Trigger
      • Click : TriggerLeft
      • Pull : TriggerAxisLeft
    • Joystick
      • Click : TurnLeft (座ってプレイする場合)
      • Position : MotionControllerThumbLeft
    • Grip
      • Click : GripLeft
    • X Button
      • Click : Open Console (お好みで)
    • Y Button
      • Click : MenuLeft (お好みで)
  • Right Oculus Touch
    • Trigger
      • Click : TriggerRight
      • Pull : TriggerAxisRight
    • Joystick
      • Click : TurnRight (座ってプレイする場合)
      • Touch : Run (お好みで)
      • Position : MotionControllerThumbRight
    • Grip
      • Click : GripRight
    • A Button
      • Click : Skill_ReleaseManazine (お好みで)
    • B Button
      • Click : Skill_FireModeChange (お好みで)
  • 未割り当て
    • Skill_RelaseSlider
    • ControllerMovementLeft
    • ControllerMovementRight

立ってプレイする場合には、自分が振り向けば良いので TurnLeft/Right は不要です。立ってプレイしないとしゃがめないので被弾してしまう場面があり、立ってプレイすることは必須なのですが...。

設定は保存できるので保存を推奨します。次回起動時、設定がクリアされてしまいます。設定を保存しておかないと、設定をやり直すことになります。

Room の使い方

概要

Android Kotlin Fundamentals 06.1 (Room) の備忘録です。ポイントとなるコードのみを抜粋しています。

目次

確認環境

  • AndroidStudio 3.5
    • compileSdkVersion 28
    • minSdkVersion 19
  • Gradle 5.4.1
  • Kotlin 1.3.11

参考情報

解説

build.gradle

buildscript {

    ext {
        kotlin_version = '1.3.11'
        archLifecycleVersion = '1.1.1'
        room_version = '2.0.0'
        coroutine_version = '1.0.0'
        gradleVersion = '3.3.0'
        navigationVersion = '1.0.0-alpha08'
        dataBindingCompilerVersion = gradleVersion // Always need to be the same.
    }

    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath "com.android.tools.build:gradle:$gradleVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}

...

app/build.gradle

...

dependencies {
    ...
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

SleepNight.kt

...

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
        @PrimaryKey(autoGenerate = true)
        var nightId: Long = 0L,

        @ColumnInfo(name = "start_time_milli")
        val startTimeMilli: Long = System.currentTimeMillis(),

        @ColumnInfo(name = "end_time_milli")
        var endTimeMilli: Long = startTimeMilli,

        @ColumnInfo(name = "quality_rating")
        var sleepQuality: Int = -1
)

SleepDatabaseDao.kt

...

@Dao
interface SleepDatabaseDao {
    @Insert
    fun insert(night: SleepNight)

    @Update
    fun update(night: SleepNight)

    @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
    fun get(key: Long): SleepNight?

    @Query("DELETE FROM daily_sleep_quality_table")
    fun clear()

    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
    fun getTonight(): SleepNight?

    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
    fun getAllNights(): LiveData<List<SleepNight>>
}
  • アノテーションは Insert, Update, Delete, Query の 4 つ
  • LiveData を戻り値にするとデータベースのデータが変更された場合に変更を通知する
    • (変更の検知はデータベース単位?テーブル単位?便利だけどパフォーマンスは問題にならない?)

SleepDatabase.kt

...

@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {

    abstract val sleepDatabaseDao: SleepDatabaseDao

    companion object {
        @Volatile
        private var INSTANCE: SleepDatabase? = null

        fun getInstance(context: Context): SleepDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(context.applicationContext, SleepDatabase::class.java, "sleep_history_database")
                            .fallbackToDestructiveMigration()
                            .build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

SleepDatabaseTest.kt

...

@RunWith(AndroidJUnit4::class)
class SleepDatabaseTest {

    private lateinit var sleepDao: SleepDatabaseDao
    private lateinit var db: SleepDatabase

    @Before
    fun createDb() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        db = Room.inMemoryDatabaseBuilder(context, SleepDatabase::class.java)
                .allowMainThreadQueries()
                .build()
        sleepDao = db.sleepDatabaseDao
    }

    @After
    @Throws(IOException::class)
    fun closeDb() {
        db.close()
    }

    @Test
    @Throws(Exception::class)
    fun insertAndGetNight() {
        val night = SleepNight()
        sleepDao.insert(night)
        val tonight = sleepDao.getTonight()
        assertEquals(tonight?.sleepQuality, -1)
    }
}
  • inMemoryDatabaseBuilder を使うことでテスト毎にデータベースをクリアする
  • allowMainThreadQueries を実行することでデータベースへの問い合わせを MainThread で実行できる様にする
    • 通常は MainThread でデータベースに問い合わせるとエラーになる

Navigation component の使い方

概要

Android Kotlin Fundamentals 03.x (Fragment と Navigation) の備忘録です。ポイントとなるコードのみを抜粋しています。

目次

確認環境

  • AndroidStudio 3.5
    • compileSdkVersion 28
    • minSdkVersion 19
  • Gradle 5.4.1
  • Kotlin 1.3.11

参考情報

解説

build.gradle

buildscript {
    ext {
        kotlin_version = '1.3.11'
        archLifecycleVersion = '1.1.1'
        gradleVersion = '3.5.0'
        supportlibVersion = '1.0.0-rc03'
        navigationVersion = '1.0.0-rc02'
        dataBindingCompilerVersion = gradleVersion // Always need to be the same.
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradleVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}
  • Safe Args Gradle plugin を classpath に追加

app/build.gradle

...
apply plugin: 'androidx.navigation.safeargs'
...
dependencies {
    ...
    implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
    implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"
    ...
}
  • Safe Args Gradle plugin を有効化
  • Navigation components 関連のライブラリを追加

activity_main.xml

...
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawerLayout"
        ...>

        <!-- サンプルでは LinearLayout の子になっている -->
        <fragment
            android:id="@+id/myNavHostFragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/navigation"
            app:defaultNavHost="true"
            ... />

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navView"
            android:layout_gravity="start"
            app:headerLayout="@layout/nav_header"
            app:menu="@menu/navdrawer_menu"
            ... />

    </androidx.drawerlayout.widget.DrawerLayout>
</layout>
  • Drawer を使用しない場合は DrawerLayout は不要
  • Navigation components を使って Fragment を切り替えるため NavHostFragment を使用
  • defaultNavHost を true に設定した Fragment が Back ボタンをインターセプト
  • NavigationView は Drawer に表示するナビゲーション
...
<navigation
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation"
    app:startDestination="@id/titleFragment">

    <fragment
        android:id="@+id/titleFragment"
        android:name="com.example.android.navigation.TitleFragment"
        android:label="fragment_title"
        tools:layout="@layout/fragment_title" >
        <action
            android:id="@+id/action_titleFragment_to_gameFragment"
            app:destination="@id/gameFragment" />
    </fragment>

    <fragment
        android:id="@+id/gameFragment"
        android:name="com.example.android.navigation.GameFragment"
        android:label="fragment_game"
        tools:layout="@layout/fragment_game" >
        <action
            android:id="@+id/action_gameFragment_to_gameOverFragment"
            app:destination="@id/gameOverFragment"
            app:popUpTo="@+id/gameFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_gameFragment_to_gameWonFragment"
            app:destination="@id/gameWonFragment"
            app:popUpTo="@+id/gameFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/gameOverFragment"
        android:name="com.example.android.navigation.GameOverFragment"
        android:label="fragment_game_over"
        tools:layout="@layout/fragment_game_over" >
        <action
            android:id="@+id/action_gameOverFragment_to_gameFragment"
            app:destination="@id/gameFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
    </fragment>

    <fragment
        android:id="@+id/gameWonFragment"
        android:name="com.example.android.navigation.GameWonFragment"
        android:label="fragment_game_won"
        tools:layout="@layout/fragment_game_won" >
        <action
            android:id="@+id/action_gameWonFragment_to_gameFragment"
            app:destination="@id/gameFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
        <argument
            android:name="numQuestions"
            app:argType="integer" />
        <argument
            android:name="numCorrect"
            app:argType="integer" />
    </fragment>

    <fragment
        android:id="@+id/aboutFragment"
        android:name="com.example.android.navigation.AboutFragment"
        android:label="fragment_about"
        tools:layout="@layout/fragment_about" />
    <fragment
        android:id="@+id/rulesFragment"
        android:name="com.example.android.navigation.RulesFragment"
        android:label="fragment_rules"
        tools:layout="@layout/fragment_rules" />
</navigation>
  • tools:layout は Design editor でレイアウトを表示するために指定
  • app:popUpTo は Back 時の遷移先
  • app:popUpToInclusive が false の場合は popUoTo の Fragment に遷移する
  • app:popUpToInclusive が true の場合は popUoTo の Fragment もバックスタックから除かれ、もう一つ前の Fragment に遷移する
    • titleFragment → gameFragment → gameOverFragment と順にスタックに積まれているので、popUpTo=gameFragment , inclusive=true だと titleFragment に遷移する
  • Safe Args が有効の場合は XXXFragmentDirections クラス (NavDirections を実装) が生成される
    • argument を指定すると XXXFragmentArgs クラスが生成され、遷移元の NavDirections のファクトリメソッドに引数が追加される
...
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/rulesFragment"
        android:title="@string/rules" />
    <item
        android:id="@+id/aboutFragment"
        android:title="@string/about" />
</menu>
  • item の id と navigation.xml で指定した fragment の id を合わせるとライブラリが良きに計らってくれる

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var drawerLayout: DrawerLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        drawerLayout = binding.drawerLayout
        val navController = findNavController(R.id.myNavHostFragment)
        NavigationUI.setupWithNavController(binding.navView, navController)
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
    }

    override fun onSupportNavigateUp(): Boolean {
        return NavigationUI.navigateUp(findNavController(R.id.myNavHostFragment), drawerLayout)
    }
}
  • setupWithNavController はスライド操作で Drawer を表示するために必要
  • setupActionBarWithNavController はアクションバーで Drawer の表示を行うために必要
  • onSupportNavigateUp メソッドはアクションバーで戻る操作を行うために必要

TitleFragment.kt

class TitleFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding = DataBindingUtil.inflate<FragmentTitleBinding>(inflater, R.layout.fragment_title, container, false)
        binding.playButton.setOnClickListener { view ->
            view.findNavController().navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
        }
        setHasOptionsMenu(true)
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.options_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        return NavigationUI.onNavDestinationSelected(item!!, view!!.findNavController())
                || super.onOptionsItemSelected(item)
    }
}
  • navigate メソッドの引数は navigation.xml における action の id でもよい
  • Safe Args を使用している場合は navigate メソッドの引数に NavDirections を使用できる
  • onNavDestinationSelected は menu の item id から navigation の fragment id を特定して遷移する

GameFragment.kt

class GameFragment : Fragment() {
    ...

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        ...
                        view.findNavController().navigate(
                                GameFragmentDirections.actionGameFragmentToGameWonFragment(
                                        numQuestions,
                                        questionIndex
                                ))
        ...
    }

    ...
}
  • Safe Args を使用しており、navigation の fragment に argument が指定されている場合には NavDirections のファクトリメソッドに引数が追加される

GameWonFragment.kt

class GameWonFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        ...
        val args = GameWonFragmentArgs.fromBundle(arguments!!)
        Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
        setHasOptionsMenu(true)
        return binding.root
    }

    private fun getShareIntent() : Intent {
        val args = GameWonFragmentArgs.fromBundle(arguments!!)
        return Intent(Intent.ACTION_SEND)
                .setType("text/plain")
                .putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
    }

    private fun shareSuccess() {
        startActivity(getShareIntent())
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.winner_menu, menu)
        if (getShareIntent().resolveActivity(activity!!.packageManager) == null) {
            menu?.findItem(R.id.share)?.isVisible = false
        }
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            R.id.share -> shareSuccess()
        }
        return super.onOptionsItemSelected(item)
    }
  • XXXFragmentArgs.fromBundle を使えば型安全を得た状態で値を受け取れる
  • onCreateOptionsMenu では ACTION_SEND に対応する Activity の存在を検証し、存在しない場合はメニューを非表示にする

無料のボイスチェンジャーを色々と試した

概要

無料のボイスチェンジャーをいくつか試した結果、VSTHost + RoVee に落ち着きました。OBS Studio と合わせて使うことを前提にしています。

目次

参考情報

解説

まずは参考情報のサイトを参照しつつ、試してみるボイスチェンジャーのソフトウェアをピックアップしました。

今回試したソフトウェアは以下の

試行

恋声

恋声は以前にも別記事で使用しています。2019年現在、最終更新は2018年になっています。

まず、恋声で良いなと思う点です。

  • インストール不要で手軽に使える
  • 男声→女声の初期設定がある
  • 声の高さ(ピッチ)と声の性質(フォルマント)の設定の値の幅が広い
    • 自分の声質(低い声)でも女声っぽくできた

しかし、不満に思う点もあります。

  • 動作が不安定

これが私としては致命的な欠点であり、使用を躊躇する点です。

Gachikoe! Core

2019年現在、絶賛開発中のボイスチェンジャーです。

良いなと思う点は以下です。

  • インストール不要で手軽に使える
  • 設定項目が少なく簡潔
  • 低遅延、高音質らしい
  • 安定して動作する

長時間使用したわけではないので、安定して動作すると言っても恋声と比較しての話です。恋声は短時間の使用でも、動作が不安定になることが頻繁にありました。

恋声に思っていた致命的な欠点は改善されますが、再び致命的な欠点があります。

  • 設定の幅が狭い
    • 自分の声質(低い声)だと女声にはならない

今後に期待のボイスチェンジャーですが、現状では私のニーズを満たすものではありません。低遅延、高音質だとしても、作り出される声がそもそもニーズに満たないと仕方がなく、次へ。

バ美声

2019年現在、こちらも絶賛開発中のボイスチェンジャーです。

良いなと思う点は Gachikoe! Core と同じです。そして、欠点も同じです。Gachikoe! Core とそっくりなコンセプトだと思いました。

こちらも今後に期待のボイスチェンジャーですが、現状では私のニーズを満たすものではなく、次を試すことにしました。

RoVee

2019年現在、最終更新が2013年となっており既に更新が止まっている様子です。他の3つのソフトウェアは単体で動作するソフトウェアでしたが、RoVeeはVSTプラグインとなっており単体では動作しません。OBS StudioにはVSTプラグインによって音声変換を拡張する機能があります。RoVeeをOSB Studioに組み込んでの使用を考えました。

試してみたところ、OSB StudioでマイクのフィルタにRoVeeを表示できましたが、設定を行うウィンドウが表示されません。設定を変更できなければ使い物になりません。調べているとOSB StudioでRoVeeを使っている方もいる様ですが、設定の変更方法までは分かりませんでした。

OSB Studioで直接使うことは断念し、VSTプラグインを組み合わせて音声変換を行うVSTHostを使うことにしました。しかし、VSTHostの公式ダウンロードサイトが表示できません。非公式と思われるサイトは見つかりました。安全性に不安はありますが、下記のサイトからダウンロードできます。

VSTHostでRoVeeを読み込ませることで音声を変換できました。

この組み合わせの良いなと思う点は以下です。

不満に思う点と言えば下記くらい。

VSTHostは今後にメンテナンスされることがなく、使えなくなるかもしれないという不安はあります。その頃には、現在開発中のボイスチェンジャーが機能拡張されていることを祈ります。

まとめ

VSTHostを使えば複数のVSTプラグインを組み合わせて様々な変換を行えます。男声を女声にするだけでなく、複数の声を混ぜたり、エコーをかけたりと組み合わせ次第では無限です。あまり注意していませんでしたが、変換による遅延もそれほど感じませんでした。無料でありながら、安定して動作しつつ、カスタマイズの自由度があり自分の声にも対応できそうだと思います。