音声認識・合成音声・自動翻訳プログラムをC#で作ってGitHubでソース公開してみました

#概要 :point_down:
> [GitHub - Kikisen-VC (音声認識・音声合成・翻訳・擬似VCツール)](https://github.com/zufall-upon/kikisen-vc)

音声認識・音声合成・翻訳・擬似VC用のツールをGitHubで公開してみました。
各所のサンプルをつまみ食いで参考にさせて頂いて組み立てた拙いソースですが、この分野に興味のある方の参考になれば幸いです。

#####2017.06.20 該当ソースを添付して更新しました。
#####2017.06.24 BingSpeechAPIに関する記述を追加しました。
#####2017.11.05 Intel RealsenseとOpenJTalkに対応してみました。

#苦労した点 :frowning2:
###GoogleCloudSpeechAPIのレスポンスが遅い！
「Uターン」、や「T字路」「okです」等、アルファベットを含む単語の認識がやたら遅かったので苦労しました。。
#####*認識精度も返ってきてるのに気づくのが遅れた・・*
```csharp:MainWindow.xaml.cs
// アルファベット混じりだと発声が遅れるので処置する
if (bContainAlpha) {
    // 信頼度がある程度以上ならば区切りと判断
    if (0.8 < note.Results[0].Stability) {
        var subtext = "";
        if (1 < note.Results.Count) {
            subtext = note.Results[1].Alternatives[0].Transcript;
            if (0 < subtext.Length) {
                subtext = Regex.Replace(subtext, @"\s", "");
                speechtxt += subtext;
            }
        }
        Dispatcher.BeginInvoke((Action)(() => {
            FuncVoicePlay(cmbOutputDevice.Items.IndexOf(_OutputDevice), speechtxt, _SpeechAPI, _say_msVolume, _say_msPitch, _say_msEmphasis, _say_msRate, _sayPitch, _saySpeed, _sayVolume, _sayEmotion);
        }));
```
認識精度と推測単語がオブジェクトに含まれているのに結構最近まで気付かず、アルファベットを含んだ認識の場合はこっちを参照することで解決しています。これでレスポンスも上々！

#####→バグってましたここ:expressionless:
```csharp:MainWindow.xaml.cs
    // 信頼度がある程度以上ならば区切りと判断
    if (0.8 < note.Results[0].Stability) {
```
これ、Stabilityはfloat型で返ってくるんでこの比較式じゃダメなんですよ・・
とりあえず動くようにはしましたが正しい修正方法は調べ中。


###ループバック音声入力のサンプリングレートが特殊！
ループバックにはWasapiLoopbackCaptureを使ってるんですが、こいつが返してくるサウンドのサンプリングレートが特殊で、
> formatChannels       : 2
> Sample Rate    : 44100
> Precision      : 25-bit
> Sample Encoding: 32-bit Floating Point PCM

みたいなものを音声認識APIが扱える形式にリサンプリングする必要がありました。

```csharp:MainWindow.xaml.cs
public byte[] Convert16(byte[] input, int length, WaveFormat format) {
    if (length == 0)
        return new byte[0];
    using (var memStream = new MemoryStream(input, 0, length)) {
        using (var inputStream = new RawSourceWaveStream(memStream, format)) {
            var sampleStream = new NAudio.Wave.SampleProviders.WaveToSampleProvider(inputStream);
            var resamplingProvider = new NAudio.Wave.SampleProviders.WdlResamplingSampleProvider(sampleStream, resampler, 16000);
            var ieeeToPCM = new NAudio.Wave.SampleProviders.SampleToWaveProvider16(resamplingProvider);
            var sampleStreams = new NAudio.Wave.StereoToMonoProvider16(ieeeToPCM);
            sampleStreams.RightVolume = 0.5f;
            sampleStreams.LeftVolume = 0.5f;
            return readStream(sampleStreams, length);
        }
    }
}
private byte[] readStream(IWaveProvider waveStream, int length) {
    byte[] buffer = new byte[length];
    using (var stream = new MemoryStream()) {
        int read;
        while ((read = waveStream.Read(buffer, 0, length)) > 0) {
            stream.Write(buffer, 0, read);
        }
        return stream.ToArray();
    }
}
// 匿名デリゲートだとエラーが出るので外だし
void _ms_wi_DataAvailable(object sender, WaveInEventArgs e) {
    try {
        if (_ms_wloop != null) {
            byte[] output = Convert16(e.Buffer, e.BytesRecorded, _ms_wloop.WaveFormat);
            _ms_wloop_ss.WriteLoop(output, 0, output.Length);
```
#####*「フフフフフ」とか返ってきて夜中怖い！*
サンプリングレートが違うものを音声認識APIに渡すと、「FFFFFFFF」などとして認識されるようです。
夜中にコーディングしててテスト中に「ふふふふふふふ」と認識文字列が表示されて無駄に怖い思いをしました。。
参考記事を参照してリサンプリングに成功し、なんとかループバック音声認識の疎通に成功したときは嬉しかったです！

###BingSpeechAPIを実装しました
苦労した点は情報が少ないこと！stackoverflowにもほぼリアルタイムでの音声認識に関する情報がなく・・
でも1記事あったのでそれをヒントにしてGoogleCloudSpeechAPIと同じやり方で実装してみました。
すなおに動いてくれて助かった・・ところで、これって推測単語辞書機能ってあるんですかね？？

```csharp:MainWindow.xaml.cs
_micClient = SpeechRecognitionServiceFactory.CreateDataClient(SpeechRecognitionMode.LongDictation, _recog_lang_set, _keyBingSAPI1);

_micClient.OnPartialResponseReceived += this.OnPartialResponseReceivedHandler;
_micClient.OnResponseReceived += this.OnMicDictationResponseReceivedHandler;
_micClient.OnConversationError += this.OnConversationErrorHandler;
_micClient.SendAudioFormat(SpeechAudioFormat.create16BitPCMFormat(16000));

var recorder = new RecordModel();
recorder.RecordDataAvailabled += (sender2, e2) => {
    if (0 < e2.Length) {
        try {
            lock (recorder) {
                _micClient.SendAudio(e2.Buffer, e2.Length);
            }
        } catch (InvalidOperationException w_e4) {
        }
    }
};
recorder.Start();

// Bing Speech API1回14秒までなので、14秒まできたら打ち切る
timer = new System.Timers.Timer(13800);
timer.Start();
timer.Elapsed += (sender2, e2) => {
    try {
        recorder.Stop();
    } catch (TaskCanceledException w_e4) {
    }
};
do {
    if (this.Worker.CancellationPending || cToken.IsCancellationRequested) {
        e.Cancel = true;
        break;
    }
    Thread.Sleep(Convert.ToInt32(Math.Round(_threadwaitsec/4)));
} while (!recorder.isStoped);

timer.Stop();
timer.Dispose();
```

#苦労している点 :bow:
###音声認識APIから帰ってきた文字列を二重にならないように確定して、発声させるのが難しい

```csharp:MainWindow.xaml.cs
var speechtxt = outtext;
if (0 < lastspeaktext.Length) {
    // 前回の発言内容とスペースを除去
    try {
        speechtxt = speechtxt.Substring(lastspeaktext.Length, speechtxt.Length - lastspeaktext.Length);
    } catch (Exception w_e4) {
        speechtxt = speechtxt.Replace(lastspeaktext, "");
        FuncWriteLogFile(w_e4.ToString());
    }
    speechtxt = Regex.Replace(speechtxt, @"\s", "");
}
```

認識API、言語によって認識の癖が違うのでそれぞれの最適化に苦労してます。
バッファのリストを作って、直近で確定した単語は消去してしまうのが最善かも？でもそのやり方は英語認識ではうまくいっていない模様。。
GoogleCloudSpeechAPIは、たまに思い出したように過去の発言を一気に出力する癖があるようです。。

###GoogleCloudSpeechAPIは外国語聞き取りと日本語聞き取りで帰ってくる文字列の癖が違う

```csharp:MainWindow.xaml.cs
var speechtxt = outtext;
if (0.8 < note.Results[0].Stability) {
    if (0 < lastspeaktext.Length) {
    }
    var subtext = "";
    if (1 < note.Results.Count) {
        subtext = note.Results[1].Alternatives[0].Transcript;
        if (0 < subtext.Length) {
            speechtxt += subtext;
        }
    }
    // 前回の発言内容を除去
    try {
        if (10 < lstLastspeaktext.Count) lstLastspeaktext.Clear();
        foreach (var tmpStr in lstLastspeaktext) {
            speechtxt = speechtxt.Replace(tmpStr, "");
        }
    } catch (Exception w_e4) {
        FuncWriteLogFile(w_e4.ToString());
    }
}
```
1個前で述べたものなんですが、英語の認識が難しいです。特に文脈の区切り方。
####*英語の文脈の区切りで翻訳させる方法が・・*
文脈の区切りが分かりません。。英語はまだ最低限読めるからチューニングしようもあるけど、中国語対応の段階になったらどうすれば・・

###ループバック入力での自動翻訳発声は、翻訳発声を認識させないようにするのが難しい
```csharp:MainWindow.xaml.cs
// ループバックの場合
MMDevice outdevice = null;
// 既定の出力をキャプチャ、出力サウンドデバイスは別の場所で別途設定
outdevice = new MMDeviceEnumerator().GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); // 既定の出力
_ms_wloop = new WasapiLoopbackCapture(outdevice);
//_ms_wloop = new WasapiLoopbackCapture(new MMDeviceEnumerator().GetDevice(MainWindow.MMDoutputDevice));
//WaveFormat format = new WaveFormat(8000, 16, 1);
//_ms_writer = new WaveFileWriter(Environment.CurrentDirectory + "\\kikisen-vc.wav", format);
_ms_wloop.DataAvailable += _ms_wi_DataAvailable;
_ms_wloop.ShareMode = AudioClientShareMode.Shared;
_ms_wloop_ss = new SpeechStreamer(100000);
```
英語音声→翻訳して日本語発声の流れで、既定の出力サウンドデバイスをキャプチャしているので翻訳がループすることがあります。。
ゲームサウンドなどのキャプチャは雑音に強いGoogleCloudSpeechAPI以外は実用的じゃないようなんであれですが、このAPIは除外文字列的なのを設定できないんですかね。。
他のスピーカーとかに翻訳を出力する形など、ユーザー側に対応してもらうのは最後の手段だと思ってますが、難しいです。

###MS Desktop Haruka等のスピーチエンジンは、頑張ってるがやっぱり少し違和感ある読み方
「エンジン音」「ヘリコプターの音」などは「おん」「おと」で読み分けて欲しいんですが、「おん」で統一されてしまう。
無料で利用させて貰ってるエンジンなんで仕方ないと思いますが、、オマケなんでWindowsのメジャーバージョンアップ以外の機会での改善はされないんでしょうね。。

###翻訳APIが有料のものしかない！
ひと通り調べてみましたが、無料のものってないみたいですね。。
エキサイト翻訳とか老舗のところは提供してないんでしょうか。
会話キャプチャでは、翻訳APIの呼び出し回数がスゴいんで、まっとうなやり方でないとBANされそうです。。

###UWPでしか使えない「ichiro」「ayumi」を使いたい
今回WPFで作りましたが、UWPアプリだと発声機能にこいつらが使えるみたいですね。
軽く調べてみた感じ、UWPって音声入出力デバイスの指定周りが難しいみたいなんですがどうなんでしょう？

---

#参考記事 :thumbsup:
書ききれないほどあるんですが、特に参考にさせて頂いた記事を紹介させて頂きます。

- 音声認識API系
    - [Google Cloud Speech APIでストリーミング音声認識してみる](http://kmycode.hatenablog.jp/entry/2017/04/09/120203)
    - [Google Cloud Speech APIを使って.NET C#でStreamingしたときのメモ](https://kiyo.space/memo-google-cloud-speech-api-csharp-streaming-grpc/)
    - [C#での音声認識は、手軽く、精度もそこそこに、既存のマイクで行うことが出来る(霊的な引用を、あなたの心に届けよう)。](http://qiita.com/exliko/items/2fff77ddd3091469cb2b)
    - [C# で日本語合成音声・音声認識をやってみよう](http://d.hatena.ne.jp/rti7743/20111215/1323965483)
    - [System.Speech でディクテーション(音声を文字列)にする](http://neareal.com/2262/)

---

- 音声キャプチャ系
    - [NAudio ライブラリでオーディオを再生する（４）](http://blog.ume108.mobi/?p=1453)
    - [NAudio Loopback Record (record what you hear through the speaker)](http://www.blakepell.com/2013-07-26-naudio-loopback-record-what-you-hear-through-the-speaker)
    - [WaveFormat cannot be set for WASAPI Loopback Capture ](https://github.com/naudio/NAudio/issues/174)
    - [Fully Managed Input Driven Resampling with NAudio in C#](http://markheath.net/post/fully-managed-input-driven-resampling-wdl)

---

- 翻訳API系
    - [Microsoft Translator APIを使ってみる](http://qiita.com/kemayako/items/21fe36005e6e729aff77)
    - [Using Google Translate in C#](https://stackoverflow.com/questions/2246017/using-google-translate-in-c-sharp)

#最後に :pray:
コンセプト賛同者募集中です。。
自分で実際にゲームに使いながらブラッシュアップしていきたいと思ってますが、生暖かい目で見られてる気がします。。（発声機能）

英語の翻訳聞き取りについても同様にゲームで使いながら進めていきますが、まずは翻訳周りになにか思い付き（ひらめき）がないと進まないかと思ってます。

~~出来るだけ無料で使えるものにしたいので、IntelRealsenseなどの取り込みのほうが先かな・・~~
~~どうやらIntelRealsenseSDKは有償配布になったようで、普通の方法ではDLできないみたいです。~~
代わりにBingSpeechAPIを実装してみました。こっちのほうがクレカ登録とかない分、気軽に試せるかも。でも認識精度はGCSに比べるといまいちですね。。

Intel Realsenseは配布が再開されたようなので追加対応してみました。
ただ、本当に情報が少ないですね・・単語辞書の対応ができてないです。ボキャブラリ用のファイルを追加すると謎のエラーが発生するようです。。情報がほしいです・・
あと発声エンジンとしてOpenJTalkに対応してみました。コマンドラインでwav書き出しさせて、wav再生するだけの無理やりな実装ですが・・htsvoiceが結構活発に公開されてるようなので音声合成に幅がでました。ひどくはないですが遅延はありますが、、wav書き出しさせないで実現する方法も探る必要ありそうです。
興味がある方はソースを見てみて下さい。