Hyper-Vを使って、お手軽字幕とまあまあリアルタイム音声合成でゲーム実況配信しよう！

# 動機
ゲーム実況をライブ配信したい！
でも生声やボイチェンじゃなくって合成音声を使いたい！
ついでに字幕編集もめんどうだから自動生成したい！
と思ったので、そこそこの精度、そこそこのリアルタイム性で字幕の生成と音声の合成をやってみました。

# こういうことです
<iframe width="560" height="315" src="https://www.youtube.com/embed/HuB_ySB2rVM?t=198" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# やっていることは一見簡単で
Windowsの音声入力機能を使ってテキストボックスに文字を入力。これを字幕と言い張ります。
そして、テキストボックス内のテキストの変化を検知して、VOICEVOXで音声を合成して即座に再生しています。
これだけなら簡単なのですが、ゲーム実況で使うために少々準備するものが多くなっています。

# 準備が多いって？
字幕と音声だけなら、Windowsに勝手についてくる音声入力、後で紹介するPowerShellのスクリプト、VOICEBOXだけで十分です。
しかしそれをゲーム実況で使おうとすると困難が生じます。
音声入力はアクティブなウィンドウに対して行われるため、常に字幕用テキストボックスをアクティブにしなければなりません。
しかし、PCでゲームをやる場合はもちろんのこと、そうでなくても配信中のチャットなどPC上の作業のために、常にアクティブにするというのは非現実的。
そこで、音声入力を仮想マシンとリモデを使って力業で解決します。
仮想マシン自体のウィンドウが非アクティブでも、仮想マシン内でテキストボックスがアクティブであれば音声入力が可能であることを利用します。

# 事前に用意するもの
- Windows11のProやEnterpriseなど、Hyper-Vが使えるエディション
- OBSなどの配信ソフト
- (Optional(※)) Microsoft PowerToys

他にも途中でインストールするものがありますが、OS以外は無料でそろいます。
ここでは、Hyper-Vの設定やOBSの使い方は詳しく解説いたしません。予めご了承ください。

(※) 後に紹介する字幕用の仮想マシンの画面を、ゲーム画面に被せて配信する場合のみ必要です。
別々に用意してOBSなどの機能で合成する場合は不要です。

# レシピ

### Hyper-Vを使えるようにする
"Hyper-V"でググればたくさん出てきますので、おそらく困ることはないかと思われます。

### Windows 11 開発環境の仮想マシンを作る
Hyper-Vマネージャーを開きます。「操作」メニューから「クイック作成」を選んでください。
クイック作成ではありがたいことに、Windowsの仮想マシンを簡単に作ることができます。「Windows 11 開発環境」を選んで仮想マシンを作成してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/6d85a0ac-bd52-1123-6049-dd37ba0c1432.png)
画面にはっきり書いてある通り、このWindowsは開発用であり評価版です。本運用したい場合は新たにWindowsを買いましょう。
作成した仮想マシンは、デフォルトのメモリ量では結構厳しいかもしれません。使い勝手とお手元のマシンのメモリと相談して、いい感じにしましょう。私は最低8GB割り当てました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/19dd69e4-1b45-49e2-65ea-5fa080d6fe2b.png)


### 仮想マシンの言語設定を日本語にする
作成したばかりのWindows 11 開発環境には日本語は入っていません。追加しておきましょう。
検索から「言語の設定」をクリックするのが早いと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/fabcdee0-769c-a88a-c660-0d867893a20d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/63762f10-cf60-e337-288f-ddb1e0d89dd1.png)

### 仮想マシンに必要なソフトをインストールする
必須のソフトは [VOICEVOX](https://voicevox.hiroshiba.jp/) だけですが、
字幕のフォントにこだわりたい場合は、そのフォントもインストールしておきましょう。
仮想マシンで音声合成、再生と字幕表示を行います。

### (あとでリモデを使う場合のみ) 仮想マシンの音声入力を有効にする
音声入力はWinキー + Hでできるようになります。
しかしリモートデスクトップの場合、デフォルトでは全画面表示の時しかWinキーがリモートに送られません。
設定を変更することもできますが、仮想マシンは用途が限られているため、通常は手元のマシンでWinキーを使いたいでしょう。
今のうちにWinキーなしでも音声入力できるようにしましょう。全画面表示でリモートデスクトップで接続してください。
(リモデをつながるようにする場合は、仮想マシン側で有効にする必要があります。)
まずWin + Hで音声入力のポップアップを出し、設定から「音声入力起動ツール」をオンにしてください。
こうすることで、入力可能な場所がアクティブになると音声入力のポップアップが出てくるので、マイクのボタンを押せば音声入力できるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/46e972ef-6d6a-56aa-8316-0943a1d8f5da.png)

### 手元のマシンに必要なソフトをインストールする
[OBS](https://obsproject.com/ja/download) などの配信ソフトをインストールしてください。

### (Optional) PowerToysをインストールして設定する
お手元のマシンに、もう一つ重要なソフトとして、Microsoft謹製の便利なツール [PowerToys](https://learn.microsoft.com/ja-jp/windows/powertoys/) をインストールしてください。
できたら起動しましょう。この中の「常に手前に表示(Always on Top)」が重要です。
そう。字幕をゲーム画面より前に出すためです。有効にしておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/828a3d0f-c675-c300-f406-ef399a00bc74.png)

### リモートオーディオを有効にして仮想マシンに接続する
仮想マシンからいったんログアウトして、拡張セッションかリモートデスクトップで接続し直します。
この時重要なポイントは2つ。
ひとつは「ローカルリソース」→「リモートオーディオ」→「設定」から、「リモートオーディオ再生」を「このコンピューターで再生する」、「リモートオーディオ録音」を「このコンピューターから録音する」にすること。仮想マシンで音声入力と再生を行うために必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/dce9c425-995a-7785-e782-b6ee040bb952.png)
もうひとつは、画面サイズを小さめにすること。仮想マシンの画面を字幕としてゲーム画面に被せるようにして使うため、大きすぎるとゲーム画面で見せたいところが見せられなくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/04ee19b4-62fd-4d14-d394-857b01709cdf.png)
PowerToysをインストールした人は、ここで仮想マシンの画面をWin + Ctrl + Tで最前面に固定しましょう。

### 仮想マシンでVOICEVOXを起動する
VOICEVOXを立ち上げると、自動的に `localhost:50021` でアクセスできるREST APIを提供するWebサーバが立ち上がります。便利ですね。音声合成ではこのAPIを使います。

### 仮想マシンで字幕兼音声合成スクリプトを起動する
お待たせしました。PowerShellで以下のスクリプトを実行してください。ps1ファイルに保存しておいて実行するのが良いと思います。解説はコメントとして書いておきますね。
```実況.ps1
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Web

$speaker = 8 # ← VOICEVOXの話者のIDを指定します。http://localhost:50021/speakers で一覧を取得できます。
$interval = 200 # ← テキストの差分を確認しにいくインターバルをmsで指定します。

# $formは入力用のウィンドウです。
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(800, 170)
$form.Topmost = $True
$form.Text = "字幕"
# $textBoxは音声入力で文字を入力するテキストボックスです。これが字幕にもなります。
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Multiline = $True
$textBox.Location = New-Object System.Drawing.Point(0, 0)
$textBox.Size = New-Object System.Drawing.Size(780, 135)
$textBox.Font = New-Object System.Drawing.Font("りいてがきN R", 28) # ← フォントとサイズです。好きなように変更してください。フォントはあらかじめインストールしておいてね。
$textBox.ForeColor = "RoyalBlue" # ← フォントの色です。好きな色にしてください。
$textBox.WordWrap = $True
$textBox.ScrollBars = 2 # 縦のスクロールバーを表示。いらないかも。
$form.Controls.Add($textBox)
$form.Add_Shown({$textBox.Select()})

$script:textPrev = ""

# $timerは、定期的にテキストボックスのテキストに変更がないかチェックするためのタイマーです。
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = $interval
$timer.Add_Tick({
    # 現在のテキストを取得します。
    $textCurrent = $textBox.Text.Trim()
    # テキストに差分があれば…
    if ($textCurrent -ne "" -and $textCurrent -ne $script:textPrev) {
        # テキストの差分だけを $text として取得します。
        # 何かの間違いでテキストが全部消えることを想定し、変更前と後でテキストが減っている場合は全体を取得します。
        $text = if ($textCurrent.Length -gt $script:textPrev.Length) { $textCurrent.Substring($script:textPrev.Length) } else { $textCurrent }
        # 音声合成のクエリを生成します。詳細を知りたい方はVOICEVOXのAPIについてお調べください。
        $query = (Invoke-WebRequest -Method POST -Uri "http://localhost:50021/audio_query?speaker=$speaker&text=$([System.Web.HttpUtility]::UrlEncode($text))").Content
        # 生成されたクエリを投げて、音声(wav)データを取得します。
        $wavStream = (Invoke-WebRequest -Method POST -Uri "http://localhost:50021/synthesis?speaker=$speaker" -ContentType "application/json" -Body $query).RawContentStream
        # 音声データを再生するプレイヤーを作成します。
        $player = New-Object Media.SoundPlayer($wavStream)
        # テキストの最後尾が画面に表示されるように、そこまでスクロールします。
        $textBox.SelectionStart = $textBox.Text.Length
        $textBox.Focus()
        $textBox.ScrollToCaret()
        # 音声を同期的に再生します。
        $player.PlaySync()
    }
    $script:textPrev = $textCurrent
})

# タイマーを有効にします。
$timer.Enabled = $True
# 入力ウィンドウをモーダルで表示させます。ウィンドウを閉じれば終了します。
$form.ShowDialog()
$timer.Enabled = $False
```

### れっつすぴーく！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/1d749e7e-d23d-8b97-f425-8b6d666046ee.png)
起動するとこのように「字幕」と書かれたテキストボックスがウィンドウが表示されるはずです。このウィンドウをアクティブにしたまま、音声入力をオンにして喋ってみましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3090735/cad5419f-6906-6012-bf54-7e16725dcebc.png)
テキストボックスに文字が表示された後、少し遅れて合成音声が再生されるはずです。

そして！
手元のマシンで他のウィンドウをアクティブにしても、この入力と再生は途切れることはありません。
ゲームや他の作業をしていても字幕と音声合成を続けることができるのです。

**これでみんな実況者だ！**

# おわりに

「結構手間じゃん。手軽にやるなら、声はボイチェンで、字幕はYouTubeに任せたらいいんじゃない？」
というご意見はごもっともです。
しかしながら、私は良いと思えるボイチェンに出会っておらず、また字幕も自分である程度好きなようにしたいという思いから、このようなシステムを作ってみました。

ここに書いてある仕組みはだいたい自分で思いつけたのですが、
超重要な一点、「ウィンドウが非アクティブでも音声入力をする方法」はこちらからヒントを頂きました。
[非アクティブでもGoogle音声入力を使い続ける方法 - BBD](https://nogunori.com/entry/2019/02/23/120000)
この記事に出会わなければ実用に耐えず、私のゲーム実況が実現しておりませんでした。
この場をお借りして感謝申し上げます。