C#
Unity
VisualStudio2015
HoloLens

HoloLensでTranslator Speech APIを使ったリアルタイム翻訳-その1Translator Speech APIの使い方

More than 1 year has passed since last update.

Translator Speech APIの癖の強さ

Microsoftサポートに確認し、新たに判明した仕様などを更新。「(2017/06/02追記)」を赤字で入れました。

Transrator Speech APIはMaicrosoft社のAzureのCognitive Serviceの1APIとして提供されています。音声送れば音声を解析し、その結果を翻訳してテキスト、合成音声で結果を返してくれます。
なかなか楽しい機能なのですが、気軽に試そうとしていろいろ壁に当たりました。まだTransrator Speech APIについての情報があまり見つからない(探すのが下手なのかもしれないですが。。。)ので自分が四苦八苦したあたりを備忘録に残します。最終的にはHoloLensに搭載のやり方も含めて書きます。

Translator Speech APIとは

Microsoft AzureのCognitive Serviceの1APIでTransrator Speech APIとして提供されています。

利用料金

おそらく個人ではF0,S0のいずれかになると思うんですが、S0使う場合青天井で課金されますので注意してください。

F0:無料

  • 2h/月

S0:従量課金

  • 12ドル/1時間

課金のルールですが、Microsoftサポートに確認したところWebsocketのオープンからクローズの時間に対して課金されます。現時点では送ったデータ量ではないとの回答をもらっています(2017/06/02追記)。

Translator Speech APIの仕様

公式にある仕様は以下のサイトに書かれています。
http://docs.microsofttranslator.com/speech-translate.html
以下はサイトの情報と、試行錯誤した部分を説明しています。

翻訳できる言語

Translator Speech APIの機能の中に対応している言語の情報として以下のものをHTTP通信で取得可能です。
資料はこのサイトになります。
http://docs.microsofttranslator.com/languages.html
* Speech-to-text: 音声からテキスト翻訳できる言語の種類(speech)
* Text translation: テキストの翻訳できる言語の種類(text)
* Text-to-speech: テキストから音声に翻訳できる言語の種類(tts)
カッコ内文字列は通信時のパラメータ「scope」で渡す値になります。これによりAPIがサポートする言語が返ります。組み合わせは、翻訳元の言語と翻訳先の言語を取得した言語で任意に構成できます。

APIの仕様

このAPIはストリーミングサービスです。

このAPIはWebSocketでストリーミングサービスとして提供されています。基本的な処理方法としては以下の通りです。
1. Translator Speech APIと接続を確立する
2. 翻訳したい音声データをストリーミングで送信する
3. 送信した音声データを文章単位で非同期にレスポンスが返ってくる
ここではまったのが2.の手順です。どうもリアルタイム翻訳を前提にしたAPIだからなのか一度に大量のデータを送信すると処理してくれません(しかも送ったデータ分は課金対象かもしれない。。。)。
ストリーミングだから当たり前といえばそれまでなのですが、例えば録音済みのデータを一気に渡すというやり方はうまく動かないです。録音済みの場合でも小出しに送る必要があります。

データの送り方

音声はwaveを使います。マイクからの音声入力考えるとwaveの方が扱いやすいという理由です。接続確立してからサービスにデータを送るのですが、最初にwaveのHeaderデータを送信します。以降は音声をサンプリングしたデータをひたすら送ります。
Headerの仕様は一般的なwaveファイルとほぼ同じなのですがストリーミングで送信するためデータサイズは不定になります。
このため音声データサイズに関する部分はサイズ0として送信します。
データのサンプリングレートは以下で設定してください。
* サンプリングレート:16kHz
* 量子化ビット:16bit
* チャネル:1
でおそらくこのTranslator Speech APIはwaveに関しては上記のデータしかうけつけない可能性があります(これ以外のレートではうまくやり取りできない)。
Microsoftサポートに確認したところ現時点では16Khz,16bit,1channelしかうけられない仕様との事です(2017/06/02追記)。

送信データの翻訳単位

基本的には文単位になります。文章の切れ目は一定時間の無音で判断するようです。リアルタイム翻訳であれば、間が開けば勝手に解析始めるという感じです。

データの受信

データの受信については翻訳が完了したデータ(文単位)から順次送られてきます。デフォルト設定のままであれば、翻訳結果は文単位で一括で送信されます。翻訳途中を一定間隔で返させることも可能です(いるのかちょっと不明)。
データは以下の順で返信されます。
1. 翻訳後のテキストデータ(JSON)
2. 翻訳後の音声データ(バイナリ)
2.の音声データについてはテキスト→音声、または音声→音声の組み合わせでの翻訳の場合に送られてくるはずなのですが、なぜか要求しなくても返ってきてるような気がします。不要であればJSON以外は捨てれば一応処理はできます。

使い方

サンプルコード交えて音声を入力して翻訳後のテキストと音声を返す説明します。
基本的な作業の流れは以下のサイトに書かれている通りです。
http://docs.microsofttranslator.com/speech-translate.html

ソースコードは以下のgithubに公開しています。最終的にはHoloLensのアプリもここに公開する予定です。

https://github.com/TakahiroMiyaura/HoloTranslatorSpeechSamples

環境その他

今回は、HoloLensに使うライブラリとしての整備もかねているので、以下の環境で実行しています。

  • IDE
    • Unity 5.5.0f3
    • Visual Studio 2015 update 3 Community Edition
  • ライブラリ関連
    • .NET版のサンプル(.NET 3.5)
      • Naudio 1.8.0 (音声データのバッファリング用、再生用)
      • Websocket-sharp 1.0.3-rc11(Unityでも使えるライブラリ)
      • LitJson 0.7.0(Unityでも使えるライブラリ)
    • UWP版のサンプル(のちのHoloLens用アプリ)
      • Microsoft.NETCore.UniversalWindowsPlatform 5.0.0(Unityがこのバージョンで出力される。)
      • Naudio 1.8.0 (音声データの再生用)

各ライブラリはすべてNugetで取得可能です。HoloLensで使用する場合はUWP版のプロジェクトをUnityからビルド後に参照して利用します1
なお、もともとはUnity上で実装するつもりで.NET 3.5版を作成していたのですが、Unityのビルド時点でUWPに変換できなったです。結局のところUnityでビルドが通る場合でもUWPの変換に失敗するシチュエーションがあるようでもう少し整理が必要といった感じです。せっかくなのでコードは残しています。

処理の流れ

処理の流れは以下になります。以降の説明はUWP版のソースコードで説明します。.NET版についても利用するライブラリが少し違うだけで行っていることは同じですのでこの説明を見ながらコード眺めれば処理は理解できると思います。

  1. Token ServiceにsubscriptionKeyを送り認証トークンを取得する。
  2. サポート言語の取得(任意:理由は後述)
  3. translator speech APIとの接続
  4. 音声データの送信
  5. 解析結果の受信

1. Token ServiceにsubscriptionKeyを送り認証トークンを取得する。

最初にCognitive Serviceの認証トークンを取得します。認証トークンは予めAzureで追加したTranslator Speech APIのSubscriptionKeyを認証サービスに送信します。サービスの接続先はhttps://api.cognitive.microsoft.com/sts/v1.0/issueTokenになります。クエリパラメータ以下の通りです。リクエストを送信すれば、レスポンスとしてトークンを取得できます。

query 設定値
Subscription-Key [サブスクリプションキー]
CognitiveTranslatorService.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

/// <summary>
///     Cognitive Service API Token Service Url
/// </summary>
private const string TokenUrl = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken?";

/// <summary>
///     Request a token to the service.
/// </summary>
/// <returns>token string.</returns>
private async Task<string> RequestToken()

{
    if (string.IsNullOrEmpty(_subscriptionKey))
        throw new ArgumentNullException("SubscriptionKey");

    var query = new StringBuilder();
    query.Append("Subscription-Key=").Append(_subscriptionKey);

    var httpClient = new HttpClient();
    var stringContent = new StringContent("");
    using (var httpResponseMessage = await httpClient.PostAsync(TokenUrl + query, stringContent))
    {
        using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync())
        {
            if (stream != null)
                using (var reader = new StreamReader(stream, Encoding.GetEncoding("UTF-8")))
                {
                    return reader.ReadToEnd();
                }
        }
    }
    return string.Empty;
}

2. サポート言語の取得

Translator speech APIがサポートしている言語の種類などを取得します。ここで取得した情報からTranslator speech APIで変換する言語の組み合わせを指定します。Translator speech APIでは、3つのパラメータ「to/from」,「voice」を指定します。
とりあえず使うのであれば、以下の設定を利用してみてください。この設定を使うことで英語→日本語の翻訳は可能です。

query 設定値
from en
to ja
voice ja-JP-Sayaka

サポート言語については以下のサービスに問い合わせます。なおこの機能はトークン
不要です。こちらもHTTP通信で取得します。サービスの接続先はhttps://dev.microsofttranslator.com/languagesになります。クエリパラメータは以下の通りです。

query 設定値
api-version 1.0
scope speech,text,tts

サンプルコードではそれぞれのscopeに分けて取得しています。以下のコードはspeechのデータをとるためのロジックです。UWPであれば、HttpClientクラスでPostAsyncメソッドを使用します。データはJSON形式の文字列表現となるので、解析してデータに格納しています。サンプルコードではUWP標準のJSON部品を使用しています。今回使っているJsonObjectは型不定の場合に自分で解析して処理するためのものです。構造わかってるならJSONシリアライザやほかのOSSのJSONライブラリでもいいと思います。
所々で出てくるasync - awaitのキーワードですが、これはC# 5.0から導入された非同期関連の機能です2

CognitiveTranslatorService.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

/// <summary>
///     Translator Language Info Service Url
/// </summary>
private const string LanguageUrl = "https://dev.microsofttranslator.com/languages?";

/// <summary>
///     Gets speech informations that Cognitive Service API can provide.
/// </summary>
/// <returns><see cref="SpeechLanguageInfo" /> object list.</returns>
public static async Task<ReadOnlyCollection<SpeechLanguageInfo>> GetSpeechLanguageInfo()
{
    IEnumerable<SpeechLanguageInfo> speechLanguageInfos = null;

    var query = new StringBuilder();
    query.Append("api-version=").Append(API_VERSION);
    query.Append("&scope=").Append("speech");

    var httpClient = new HttpClient();
    using (var httpResponseMessage = await httpClient.GetAsync(LanguageUrl + query))
    {
        using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync())
        {
            if (stream != null)
                using (var reader = new StreamReader(stream, Encoding.GetEncoding("UTF-8")))
                {
                    var json = reader.ReadToEnd();
                    var jsonObject = JsonObject.Parse(json);
                    speechLanguageInfos = jsonObject["speech"].GetObject().Select(
                        x => new SpeechLanguageInfo()
                        {
                            LocaleId = x.Key,
                            Language = x.Value.GetObject()["language"].GetString(),
                            Name = x.Value.GetObject()["name"].GetString()
                        });
                }
        }
    }
    return new ReadOnlyCollection<SpeechLanguageInfo>(speechLanguageInfos.ToArray());
}

scopeは複数設定可能でカンマ区切りで送ればまとめてJSON形式で結果が返ります。形式は以下のようなものです。

speechデータの例(抜粋)
{
  "speech":{
    "ar-EG":{
      "name":"Arabic",
      "language":"ar"
    },
    .....
    "zh-TW":{
      "name":"Chinese Traditional",
      "language":"zh-Hant"
    }
  }
}
textデータの例(抜粋)
{
  "text":{
    "af":{
      "name":"Afrikaans",
      "dir":"ltr"
    },
    .....
    "zh-Hant":{
      "name":"Chinese Traditional",
      "dir":"ltr"
    }
  }
}
ttsデータの例(抜粋)
{
  "tts":{
    "ar-EG-Hoda":{
      "gender":"female",
      "locale":"ar-G",
      "languageName":"Arabic",
      "displayName":"Hoda",
      "regionName":"Egypt",
      "language":"ar"},
    .....
    "zh-TW-Zhiwei":{
      "gender":"male",
      "locale":"zh-TW",
      "languageName":"Chinese Traditional",
      "displayName":"Zhiwei",
      "regionName":"Taiwan",
      "language":"zh-Hant"}
  }
}

3. translator speech APIとの接続

必要な情報が集まったら、translator speech APIに接続を行います。UWPでWebSocket用に提供されているクラスはStreamWebSocketMessageWebSocketの2種類ありますが、今回はMessageWebSocketクラスを使用します。
使い方はすごく簡単で、インスタンス化の後初期設定を行い接続すれば完了します。

CognitiveTranslatorService.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

/// <summary>
///     Translator WebSocket Url
/// </summary>
private const string SpeechTranslateUrl = @"wss://dev.microsofttranslator.com/speech/translate?";

/// <summary>
///     Connect to the server before sending audio
///     It will get the authentication credentials and add it to the header
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="voice"></param>
/// <returns></returns>
public async Task Connect(string from, string to, string voice)
{
    webSocket = new MessageWebSocket();

    webSocket.SetRequestHeader("Authorization", "Bearer " + bearerToken);

    var query = new StringBuilder();
    query.Append("from=").Append(from);
    query.Append("&to=").Append(to);
    if (!string.IsNullOrEmpty(voice))
        query.Append("&features=texttospeech&voice=").Append(voice);
    query.Append("&api-version=").Append(API_VERSION);


    webSocket.MessageReceived += WebSocket_MessageReceived;

    // connect to the service
    await webSocket.ConnectAsync(new Uri(SpeechTranslateUrl + query)); 
}

最初に引数なしでインスタンス化を行います。そのあとHeader情報に認証情報を付与します。これは先ほど取得した"Bearer "+認証トークン文字列をAuthorizationヘッダに付与するだけです。あとはAPIの接続先としてwss://dev.microsofttranslator.com/speech/translateと翻訳する言語の設定として以下のクエリを設定します。

query 必須 設定値
api-version 1.0
to 翻訳元の言語(例:en)
from 翻訳後の言語(例:ja)
voice 合成音声のID(例:ja-JP-Sayaka)
features texttospeech3

なお、ヘッダ情報を書き換えできないライブラリを利用する場合は、Authヘッダの代わりにクエリとしてaccess_tokenで認証トークンを渡せば正しく処理されます(詳細は公式資料を参照)。

4. 音声データの送信

処理の実装

音声データの送信については、Waveファイルを同じ形式でデータを送信します。まず最初にwaveのヘッダー情報を送信し、続いて音声データを送ります。
ヘッダについてはMicrosoft社のドキュメントに書いている通りです。ロジックで表現すると次のような形になります。先にも書きましたが、16000Hz,16bit,1channelでヘッダはセットしてください(送るデータもこのレートになるようにします)。

CognitiveTranslatorService.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

/// <summary>
///     number of the wave audio channels.
/// </summary>
private readonly short _channels;

/// <summary>
///     number of the wave audio sample rate.
/// </summary>
private readonly int _sampleRate;

/// <summary>
///     number of the wave audio bit per sample.
/// </summary>
private readonly short _bitsPerSample;

/// <summary>
///     Create a RIFF Wave Header for PCM 16bit 16kHz Mono
/// </summary>
/// <returns></returns>
private byte[] GetWaveHeader()
{
    var extraSize = 0;
    var blockAlign = (short) (_channels * (_bitsPerSample / 8));
    var averageBytesPerSecond = _sampleRate * blockAlign;

    using (var stream = new MemoryStream())
    {
        var writer = new BinaryWriter(stream, Encoding.UTF8);
        writer.Write(Encoding.UTF8.GetBytes("RIFF"));
        writer.Write(0);
        writer.Write(Encoding.UTF8.GetBytes("WAVE"));
        writer.Write(Encoding.UTF8.GetBytes("fmt "));
        writer.Write(18 + extraSize); 
        writer.Write((short) 1);
        writer.Write(_channels);
        writer.Write(_sampleRate);
        writer.Write(averageBytesPerSecond);
        writer.Write(blockAlign);
        writer.Write(_bitsPerSample);
        writer.Write((short) extraSize);

        writer.Write(Encoding.UTF8.GetBytes("data"));
        writer.Write(0);

        stream.Position = 0;
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        return buffer;
    }
}

WebSocketにデータを送信する場合はMessageWebSocketクラスのOutputStreamプロパティにデータを書き込めば非同期で送信されます。書き込む毎にデータを送信するため非効率なので、定期的にこのStreamに書き込むためにバッファリングできるラッパークラスを利用します。UWPではDataWriterクラスがそれです。このクラスはWriteメソッドでデータをバッファリングし、StoreAsyncメソッドで元のStreamクラスに対してバッファリングされたデータの書き込みを実施するという便利なものです。このStoreAsyncメソッドを一定間隔で呼び出すようにタイマーで制御しておき、音声データについてはDataWriterのWriteメソッドで随時書き込めば非同期でリアルタイムにデータを送信する仕組みが実現します。

CognitiveTranslatorService.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

// setup the data writer
dataWriter = new DataWriter(webSocket.OutputStream);
dataWriter.ByteOrder = ByteOrder.LittleEndian;
dataWriter.WriteBytes(GetWaveHeader());

//// flush the dataWriter periodically
_connectionTimer = new Timer(async s =>
    {
        if (dataWriter.UnstoredBufferLength > 0)
        {
            await dataWriter.StoreAsync();
        }
         // reset the timer
        _connectionTimer.Change(TimeSpan.FromMilliseconds(250), Timeout.InfiniteTimeSpan);
    },
    null, TimeSpan.FromMilliseconds(250), Timeout.InfiniteTimeSpan);          

DataWriterのラッピングについてはMessageWebSocketのOutputStreamを引数に渡すだけで実現できます。定期的にバッファリングしたデータをOutputStreamに書き出すために、System.Threading.Timerクラスを使用します。このクラスは指定されたメソッドを一定時間後に一定時間間隔で実行することができます。上記の実装では250ms後に1回だけ実行するように設定しています。250ms後にs=>{}内の処理が呼び出されるのですが、この最後のところで、250ms後に1度実行するようにTimerクラスの再設定を行うことで250ms毎に繰り返し実行するようにしています。

サンプルデータの書き込み

サンプルコードには何かのWaveファイルを読み込んでサービスに書き込むロジックも記載しています。waveファイルを扱いやすくするためにNaudioライブラリを使用しています。後述の受信データの再生にもNaudioを使用しています。
waveのRAWデータを取得して、量子化ビットに合わせてサンプリング値に変換しサーバに送信しています。データは小出しに送るために適当な間隔で少しずつ送るロジックを記載しています。

MainPage.xaml.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

        using (var waveStream = new FileStream(file, FileMode.Open))
        {
            var reader = new RawSourceWaveStream(waveStream, new WaveFormat(16000, 16, 1));

            var buffer = new byte[reader.Length];
            var bytesRead = reader.Read(buffer, 0, buffer.Length);

            var samplesL = new float[bytesRead / reader.BlockAlign];

            switch (reader.WaveFormat.BitsPerSample)
            {
                case 8:                    for (var i = 0; i < samplesL.Length; i++)
                        samplesL[i] = (buffer[i * reader.BlockAlign] - 128) / 128f;
                    break;

                case 16: 
                    for (var i = 0; i < samplesL.Length; i++)
                        samplesL[i] = BitConverter.ToInt16(buffer, i * reader.BlockAlign) / 32768f;
                    break;

                case 32:
                    for (var i = 0; i < samplesL.Length; i++)
                        samplesL[i] = BitConverter.ToSingle(buffer, i * reader.BlockAlign);
                    break;
            }
            var w = new byte[16000];
            for (var i = 0; i < w.Length; i++)
                w[i] = 0;
            var data = 1000;
            //この部分は再生するwaveに応じて変更してください。
            //サンプルでは3~4秒程度のデータを送っていました。
            service.AddSamplingData(buffer, 0, 32000);
            await Task.Delay(data);
            service.AddSamplingData(buffer, 32000, 32000);
            await Task.Delay(data);
            service.AddSamplingData(buffer, 64000, 32000);
            await Task.Delay(data);
            service.AddSamplingData(buffer, 96000, buffer.Length - 96000);
            await Task.Delay(data);
            //無音データを意図的に送っています。
            service.AddSamplingData(w, 0, w.Length);
        }

5. 解析結果の受信

データの送信が問題なく行われて解析が実行されると処理結果が返ってきます。合成音声での翻訳結果を受け取る場合は2回レスポンスがあります。
1回目は送信データの解析結果とその翻訳後の情報がJSON形式で取得できます。2回目のデータはバイナリのwave形式の音声データとなっています。サンプルは音声データをNAudioに渡すことで音声出力しています。
データの振り分けについてはUWPのライブラリはかなり簡単です。メッセージ受信に発生するイベントのMessageWebSocketMessageReceivedEventArgsクラスにはMessageType プロパティというメッセージの種類がわかるものがあります。これでバイナリかどうか判断すればテキスト形式のデータか音声データか判別可能です。なお、ほかのWebSocket部品を利用する場合は最初の4バイトが"RIFF"のデータになっているかを確認すればOKです。もしRIFFなら音声データが受信できているので音声処理を行います。

MainPage.xaml.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

/// <summary>
/// An event that indicates that a message was received on the MessageWebSocket object.
/// </summary>
/// <param name="sender">The event source.</param>
/// <param name="args">The event data. If there is no event data, this parameter will be null.</param>
private void WebSocket_MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args)
{
    if (OnRootMessage != null)
        OnRootMessage(sender, args);

    if (args.MessageType == SocketMessageType.Binary)
    {
        if (OnVoiceMessage != null)
            OnVoiceMessage(sender, args);
    }
    else
    {
        if (OnTextMessage != null)
            OnTextMessage(sender, args);
    }
}

テキストデータの取得

テキストデータについては以下のような形式でデータが返ってきます。必要な部分を取り出して利用する形です。サンプルコードは特に加工せずにデータを出力します。

{
  type: "final"
  id: "XXXXX",
  recognition: "[理解した文章]", 
  translation: "[理解した文章に対する翻訳データ]",
  audioStreamPosition: xxxxx,
  audioSizeBytes: xxxxx,
  audioTimeOffset: xxxxx,
  audioTimeSize: xxxxx
}
音声データの取得

音声データはwaveフォーマットに従って送られてきます。ですので例えばファイルで保存すればそのまま再生できます。今回のサンプルではファイル出力なしにNaudio使って再生しています。

MainPage.xaml.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

private void OnVoiceMessage(object sender, MessageWebSocketMessageReceivedEventArgs messageEventArgs)
{
    using (var reader = messageEventArgs.GetDataStream())
    using (var stream = reader.AsStreamForRead())
    using (var mStream = new MemoryStream())
    {
        var bufferSize = 32000;
        var bytes = new List<byte>();
        var buf = new byte[bufferSize];
        var length = stream.Read(buf, 0, buf.Length);
        while (length - bufferSize == 0)
        {
            bytes.AddRange(buf);
            length = stream.Read(buf, 0, buf.Length);
        }
        if (length > 0)
            bytes.AddRange(buf.Take(length).ToArray());

        var fullData = bytes.ToArray();
        mStream.Write(fullData, 0, fullData.Length);
        mStream.Position = 0;
        var bitsPerSampleBytes = fullData.Skip(34).Take(2).ToArray();
        var channelBytes = fullData.Skip(22).Take(2).ToArray();
        var samplingBytes = fullData.Skip(24).Take(4).ToArray();
        var bitsPerSample = BitConverter.ToInt16(bitsPerSampleBytes, 0);
        var channel = BitConverter.ToInt16(channelBytes, 0);
        var samplingRate = BitConverter.ToInt32(samplingBytes, 0);

        using (var player = new WasapiOutRT(AudioClientShareMode.Shared, 250))
        {
            player.Init(() =>
            {
                var waveChannel32 =
                    new WaveChannel32(new RawSourceWaveStream(mStream,
                        new WaveFormat(samplingRate, bitsPerSample, channel)));
                var mixer = new MixingSampleProvider(new[] {waveChannel32.ToSampleProvider()});

                return mixer.ToWaveProvider16();
            });

            player.Play();
            while (player.PlaybackState == PlaybackState.Playing)
            {
            }
        }
    }
}

最後に

Translator Speech APIの仕組みがまだあまり情報としてないので、分量は多くなりましたが何かの参考になれば幸いです。基本的にはこのUWPのライブラリを使いHoloLensのマイクからリアルタイムで集音した音声データをTranslator Speech APIに送信し、結果を文字でHoloLensに表示することで今回のものは実現しています。また、ライブラリ自体は使いまわせるはずなのでいろいろなアプリにも組み込めると思います。
その2ではHoloLensからリアルタイムで音声を集音する方法を書きたいと思います。
ほんとはその2でHoloLensで実装にするつもりでしたが、おそらく音声のリアルタイム集音はそれだけで何かに使える可能性が高いので別途整理したいなと思った次第です。


  1. Visual Studio 2015 update 3 Community EditionでUWPのプロジェクトを作成するとMicrosoft.NETCore.UniversalWindowsPlatformのバージョンは5.2.3です。 

  2. asyncはメソッドの修飾子で「非同期処理を伴っている」メソッドであることを示すものになります。この場合のメソッドも呼び出し元では非同期で処理され、戻り値はTaskクラス(戻り値なし)またはTask<戻り値の型>クラスで定義します。呼び出す場合は普通のメソッドと同じ呼び出し方をするだけで非同期で実行されます。このメソッドを呼ぶときに結果を待つ必要がある場合は呼び出し時にawait を付けます。awaitを付けると不思議なことにジェネリックで指定した型が返ってきます。注意点としてawaitを付けた処理を書いたメソッドはasync修飾子が必要になります。awaitを使えない事情がある場合(たいていUnityと連携したときに起きる)はMethodAsync().GetWaiter().GetResult()と書くと同期的に動作します。ただし、同期的に書いたときはスレッドが完全に待ち状態になりこれが仇になるパターンが多いです。非同期処理を一切使っていない自分のライブラリを非同期用に提供するようなパターンでは大丈夫そうですが、.NETの提供する非同期メソッドで同期処理書くとデッドロックっぽい動きしてる気が。。。(よく遭遇するので、同期にして異常に待ちが発生するのであれば同期処理はやめてフラグで待つなど原始的な方法に逃げたほうが幸せになるかもです) 

  3. voice指定時のみ