2017年3月16日木曜日

AndroidでWindowsの音声だけを(概ね)遅滞なく再生させたい。(2)

前承

ffmpegの改造をしてaccept時に送信バッファをクリアすりゃいい、というのもありますが、どう考えてもwindows版ffmpegをビルドする環境を構築するより自分でdshow録音->andoroidに送信->androidで再生というプログラムを作ったほうが猛烈に早いことは自明なので、そうしました。

ffmpegではdshowですが、今回の要件としては画像はいらんのでWindows Core Audio(WASAPI)とかDirect Soundとかでキャプチャできればいいわけです。


まずwindows側で接続してきた相手に録音データ(PCM)をポイポイ投げつけるプログラムを書きます。
なぜなら、Android側にはPCMデータをそのまま再生してくれるモジュールが標準で用意されているため、加工は一切必要なく何も考えずに送り付けてしまえるのです。

ただ、接続方向はAndroid->PCのほうがいいでしょう。
PCのほうはひたすらキャプチャしつつlistenしておいて、Androidさんの接続欲がこれ以上ないほど高まってついにconnectしてきたときに優しくacceptしてあげて、accept直後のキャプチャデータから送信を開始したほうがトラブルが少ないように思います。
javaのソケット回りってしょぼい(個人的感想です)からサーバ側になりたくないってのもあります。
なお、今回のサンプルでは1回acceptするとdisconnectするまでacceptしません。
まあちょっと工夫すれば複数台同時配信ができますが、そうなるともう求めるものが違う気がします。

で、キャプチャについてはAPIやサービスを問わずそれぞれWindows SDKに飛び切り上等なサンプルがあるのでそれを改造すればいいです(Microsoft純正のDirectSoundの録音「だけ」のサンプルは現在MSDNに入ってないと一部入手できなくなっていますが、なにもDirectXSDKやWindowsSDKだけがサンプルではないので、優れたサンプルの入手性は高いです)。
ま、audio capture sample +使用したいAPI名や言語名をつけて検索すれば山ほど出てきます。

WASAPIによる録音サンプルならCaptureSharedEventDrivenがお勧めです。
解像度も高いしレイテンシも小さくできます・・・が、実際にはこのケースで利用するには微妙かも知れません。

余程古いか、あるいは余程高性能な場合を除いて、一般的なサウンドカードを挿している場合、おそらく、32bit浮動小数点でしかキャプチャさせてくれないと思います。
PC内部で利用する分には大変結構なのですが、この形式の場合、AndroidではAPIレベル21以前のOSの場合16bitか8bitに変換しないと再生できません。つまり、XperiaAではそのままですと再生できないということになります。
XperiaA以外では手持ちの機器としてMediaPad T2があり、これは再生可能ですが、仮に32bitのまま投げるにしても、当然データ量も16bitの倍となり通信負荷もAndroid側での再生負荷もぐっと上がります。
まあさすがにオーバースペック過ぎだと思いますので、16ビットに変換するほうが無難かと思います。
変換といっても録音バッファからのコピー時にfloat値を32768倍するだけですしね。

また、WASAPIを使うと、確かにデバイスに尋ねると答えてくれるレイテンシは非常に小さいので魅力的です・・・が、あまり小さいレイテンシで回しても通信や別機で再生するわけですからあんまり役に立たないかもしれません。

まあ、やってみましょう。

まずPC側です。
WASAPIを使ってデバイスの標準周期で録音->送信を行います。
標準周期は大抵のデバイスで10000000ナノ秒=10ミリ秒となっているはずですのでこいつでいってみたいと思います。10msもあればまあ32bit->16bitの変換もバッファコピーも十分でしょう。
通信手順は、
  1. Androidからの接続を待つ
  2. 録音形式(ステレオなのかモノラルなのか、サンプリングレートなど)をAndroidに渡す
  3. AndroidにPCMデータを送る
  4. 切断するまで3に戻る
とまあ、ミもフタもない方式でいってみます。

まず、WASAPICaptureSharedEventDriven.cppにmain()があるので、Wavファイルとして保存する箇所をばっさり削って通信スレッドとキュー管理を兼ねたクラスを仕込んで起動します。
次に、CWASAPICaptureクラスではDoCaptureThread()でキャプチャしたデータをひたすら通信スレッドに渡すキューに追加するだけの処理を行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
DWORD CWASAPICapture::DoCaptureThread()
{
  bool stillPlaying = true;
  HANDLE waitArray[3] = {_ShutdownEvent, _StreamSwitchEvent, _AudioSamplesReadyEvent };
  HANDLE mmcssHandle = NULL;
  DWORD mmcssTaskIndex = 0;
 
  HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
  if (FAILED(hr))
  {
    printf("Unable to initialize COM in render thread: %x\n", hr);
    return hr;
  }
 
  if (!DisableMMCSS)
  {
    mmcssHandle = AvSetMmThreadCharacteristics(L"Audio", &mmcssTaskIndex);
    if (mmcssHandle == NULL)
    {
      printf("Unable to enable MMCSS on capture thread: %d\n", GetLastError());
    }
  }
  while (stillPlaying)
  {
     // HRESULT hr;
    DWORD waitResult = WaitForMultipleObjects(3, waitArray, FALSE, INFINITE);
    switch (waitResult)
    {
    case WAIT_OBJECT_0 + 0:  // _ShutdownEvent
      stillPlaying = false;    // We're done, exit the loop.
      break;
    case WAIT_OBJECT_0 + 1:  // _StreamSwitchEvent
      //
      //  We need to stop the capturer, tear down the _AudioClient and _CaptureClient objects and re-create them on the new.
      //  endpoint if possible.  If this fails, abort the thread.
      //
      if (!HandleStreamSwitchEvent())
      {
        stillPlaying = false;
      }
      break;
    case WAIT_OBJECT_0 + 2:  // _AudioSamplesReadyEvent
      //
      //  We need to retrieve the next buffer of samples from the audio capturer.
      //
      BYTE *pData;
      UINT32 framesAvailable;
      DWORD  flags;
 
      //
      //  Find out how much capture data is available.  We need to make sure we don't run over the length
      //  of our capture buffer.  We'll discard any samples that don't fit in the buffer.
      //
      hr = _CaptureClient->GetBuffer(&pData, &framesAvailable, &flags, NULL, NULL);
      if (SUCCEEDED(hr))
      {
         // UINT32 framesToCopy = min(framesAvailable, static_cast<UINT32>((_CaptureBufferSize - _CurrentCaptureIndex) / _FrameSize));
        if (framesAvailable != 0)
        {
          //
          //  The flags on capture tell us information about the data.
          //
          //  We only really care about the silent flag since we want to put frames of silence into the buffer
          //  when we receive silence.  We rely on the fact that a logical bit 0 is silence for both float and int formats.
          //
          if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
          {
            //
            //  Fill 0s from the capture buffer to the output buffer.
            //
            CSockThread::instance->addSendData ( NULL, (DWORD)(framesAvailable*_FrameSize ));
          }
          else
          {
            //
            //  Copy data from the audio engine buffer to the output buffer.
            //
            //CopyMemory(&_CaptureBuffer[_CurrentCaptureIndex], pData, framesToCopy*_FrameSize);
            CSockThread::instance->addSendData ( pData, (DWORD)( framesAvailable*_FrameSize ) );
 
          }
          //
          //  Bump the capture buffer pointer.
          //
          //_CurrentCaptureIndex += framesToCopy*_FrameSize;
 
        }
        hr = _CaptureClient->ReleaseBuffer(framesAvailable);
        if (FAILED(hr))
        {
          printf("Unable to release capture buffer: %x!\n", hr);
        }
      }
      break;
    }
  }
  if (!DisableMMCSS)
  {
    AvRevertMmThreadCharacteristics(mmcssHandle);
  }
 
  CoUninitialize();
  return 0;
}

最後に、CSockThread(今回追加)ではCWASAPICapture::DoCaptureThread()からもらったキューをひたすらAndroidに放り投げます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#include <winsock2.h>
#include <windows.h>
#include <stdlib.h>
 
#include "SockThread.h"
 
CSockThread* CSockThread::instance = NULL;
 
CSockThread::CSockThread ()
{
  instance = this;
  m_hThreadAccept = NULL;
  InitializeCriticalSection ( &m_criticalSection );
  m_hEndEvent = CreateEvent ( NULL, TRUE, FALSE, NULL );
  m_hSendRequestEvent = CreateEvent ( NULL, TRUE, FALSE, NULL );
  ResetEvent ( m_hEndEvent );
  ResetEvent ( m_hSendRequestEvent );
}
 
CSockThread::~CSockThread ()
{
  SetEvent ( m_hEndEvent );
  if(m_hThreadAccept != NULL){
    WaitForSingleObject ( m_hThreadAccept, INFINITE );
    CloseHandle ( m_hThreadAccept );
  }
  CloseHandle ( m_hEndEvent );
  CloseHandle ( m_hSendRequestEvent );
  DeleteCriticalSection ( &m_criticalSection );
}
DWORD WINAPI CSockThread::threadMain ( LPVOID* param )
{
  CSockThread* pcst = (CSockThread*)param;
  pcst->accept_main ( param );
  return 0;
}
void CSockThread::start ()
{
  DWORD threadid;
  m_hThreadAccept = CreateThread ( NULL, 0, (LPTHREAD_START_ROUTINE)threadMain, this, 0, &threadid );
}
void CSockThread::stop ()
{
  SetEvent ( m_hEndEvent );
  if(m_hThreadAccept != NULL){
    WaitForSingleObject ( m_hThreadAccept, INFINITE );
    CloseHandle ( m_hThreadAccept );
    m_hThreadAccept = NULL;
  }
}
void CSockThread::accept_main ( LPVOID * )
{
  HANDLE hAcceptEvent;
  WSANETWORKEVENTS wwe;
  hAcceptEvent = WSACreateEvent ();
  int ret;
 
  SOCKET listensock = createlistensock ( 1234 );
 
  ret = WSAEventSelect ( listensock, hAcceptEvent, FD_ACCEPT );
  if(SOCKET_ERROR == ret){
    printlog ( LOG_ERR, GetLastError (), L"WSAEventSelectエラー" );
    WSACloseEvent ( hAcceptEvent );
    return ;
  }
 
  while(true){
    DWORD dwRet;
    HANDLE phs[ 3 ];
    phs[ 0 ] = m_hEndEvent;
    phs[ 1 ] = hAcceptEvent;
 
    dwRet = WaitForMultipleObjects ( 2, phs, FALSE, INFINITE );
    if(dwRet == WAIT_OBJECT_0 + 1){
      WSAEnumNetworkEvents ( listensock, hAcceptEvent, &wwe );
      if(wwe.lNetworkEvents & FD_ACCEPT){
        SOCKET sock = waitaccept ( listensock, NULL );
        bIsAccepted = true;
        comm_main ( (LPVOID*)&sock );
        bIsAccepted = false;
        clearSendData ();
      }
    }
    else{
      // accept要求以外はおわり
      break;
    }
  }
  WSACloseEvent ( hAcceptEvent );
}
 
void CSockThread::comm_main ( LPVOID * param )
{
  SOCKET sock = *( (SOCKET*)param );
 
  bool loopflag = true;
  if(sendWaveInfo ( sock ) <= 0){
    loopflag = false;
  }
  while(loopflag){
    DWORD dwRet;
 
    HANDLE hevs[ 2 ] = { m_hEndEvent, m_hSendRequestEvent };
    dwRet = WaitForMultipleObjects ( 2, hevs, FALSE, INFINITE );
    if(dwRet == WAIT_OBJECT_0 + 1){
      //送信要求
      list<SENDDATA>* sendlist = getSendData ();
      if(sendlist == NULL){
        continue;
      }
      list<SENDDATA>::iterator ite;
 
      SENDHEAD head;
 
      for(ite = sendlist->begin(); ite != sendlist->end(); ite++){
        if(m_waveFormatEx.wBitsPerSample == 32){
          // float pcm->16bit pcm
          int frame = ( *ite ).len / ( sizeof ( float ) * 2 );
          DWORD sendlen = frame * ( sizeof ( WORD ) * 2 );
          float* pfloat = (float*)( ( *ite ).buf );
          WORD* pword =  (WORD*)( ( *ite ).buf );
          for(int j = 0; j < frame * 2; j++){
            float f = pfloat[ j ] *= 32768.0f;
            pword[ j ] = (WORD)f;
          }
          ( *ite ).len = sendlen;
        }
 
        head.id = RIFFCC ( 'F*CK' );
        // 日付とサイズのendian変換
        head.sentDate.wYear = _byteswap_ushort ( ( *ite ).addedDate.wYear );
        head.sentDate.wMonth = _byteswap_ushort ( ( *ite ).addedDate.wMonth );  //1 月は 1.
        head.sentDate.wDayOfWeek = _byteswap_ushort ( ( *ite ).addedDate.wDayOfWeek );
        head.sentDate.wDay = _byteswap_ushort ( ( *ite ).addedDate.wDay );
        head.sentDate.wHour = _byteswap_ushort ( ( *ite ).addedDate.wHour );
        head.sentDate.wMinute = _byteswap_ushort ( ( *ite ).addedDate.wMinute );
        head.sentDate.wSecond = _byteswap_ushort ( ( *ite ).addedDate.wSecond );
        head.sentDate.wMilliseconds = _byteswap_ushort ( ( *ite ).addedDate.wMilliseconds );
        head.waveDatLen = _byteswap_ulong ( ( *ite ).len );
 
        if(sock != INVALID_SOCKET){
          if(socksend ( sock, (char*)&head, sizeof ( SENDHEAD ), 0 ) < 0){
            // なんかエラー
            closesocket ( sock );
            sock = INVALID_SOCKET;
            break;
          }
          // wavデータの送信
          if(socksend ( sock, (char*)(*ite).buf, ( *ite ).len, 0 ) < 0){
            // なんかエラー
            closesocket ( sock );
            sock = INVALID_SOCKET;
            break;
          }
        }
         
        delete ( *ite ).buf;
      }
      delete sendlist;
      if(sock == INVALID_SOCKET){
        //送信異常が発生したのでおわる
        break;
      }
    }
    else{
      // 送信要求以外はおわり
      break;
    }
  }
}
 
void CSockThread::setWaveInfo ( WAVEFORMATEX* pw )
{
  m_waveFormatEx = *pw;
}
 
void CSockThread::clearSendData ()
{
  if(bIsAccepted == false){
    list<SENDDATA>* droplist = NULL;
    EnterCriticalSection ( &m_criticalSection );
    if(wavdatalist != NULL&&wavdatalist->size () != 0){
      droplist = wavdatalist;
      wavdatalist = NULL;
    }
    LeaveCriticalSection ( &m_criticalSection );
    if(droplist != NULL){
      list<SENDDATA>::iterator ite;
      for(ite = droplist->begin (); ite != droplist->end (); ite++){
        delete ( *ite ).buf;
      }
      delete droplist;
    }
  }
}
void CSockThread::addSendData ( void* wavdata, DWORD len )
{
  if(bIsAccepted == false){
    clearSendData ();
    return;
  }
  SENDDATA senddata;
  GetSystemTime ( &senddata.addedDate );
  senddata.len = len;
  senddata.buf = new char[ len ];
  if(wavdata){
    CopyMemory ( senddata.buf, wavdata, len );
  }
  else{
    if(m_waveFormatEx.wBitsPerSample == 8){
      FillMemory ( senddata.buf, len, 0x80 );
    }
    else{
      ZeroMemory ( senddata.buf, len );
    }
  }
 
  EnterCriticalSection ( &m_criticalSection );
  if(wavdatalist == NULL){
    wavdatalist = new list<SENDDATA> ();
  }
  wavdatalist->push_back ( senddata );
  SetEvent ( m_hSendRequestEvent );
  LeaveCriticalSection ( &m_criticalSection );
 
}
list<SENDDATA>* CSockThread::getSendData ()
{
  list<SENDDATA>* retlist = NULL;
  EnterCriticalSection ( &m_criticalSection );
  ResetEvent ( m_hSendRequestEvent );
  retlist = wavdatalist;
  wavdatalist = NULL;
  LeaveCriticalSection ( &m_criticalSection );
  return retlist;
}
int CSockThread::sendWaveInfo ( SOCKET sock ){
  struct WAVEINFO{
    DWORD id;
    WORD wBitsPerSample;
    WORD nChannels;
    DWORD nSamplesPerSec;
  }wi;
 
  wi.id = RIFFCC ( 'f*ck' );
  wi.wBitsPerSample = _byteswap_ushort ( m_waveFormatEx.wBitsPerSample == 32 ? 16 : m_waveFormatEx.wBitsPerSample );
  wi.nChannels = _byteswap_ushort ( m_waveFormatEx.nChannels );
  wi.nSamplesPerSec = _byteswap_ulong ( m_waveFormatEx.nSamplesPerSec );
  return socksend ( sock, (char*)&wi, sizeof ( wi ), 0 );
}

windows側はこれでおしまいです。
サンプルが優れているのであっさり終了です。
電文のマジックナンバーについてはあまり気にしないでください。なお、小文字のマジックナンバーのほうがaccept直後に投げる録音形式伝達電文で、大文字のほうが毎回投げるPCMデータの電文です。

ちょっと長くなりましたので、Android側は次回とさせていただきます。

0 件のコメント:

コメントを投稿