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の変換もバッファコピーも十分でしょう。
通信手順は、
- Androidからの接続を待つ
- 録音形式(ステレオなのかモノラルなのか、サンプリングレートなど)をAndroidに渡す
- AndroidにPCMデータを送る
- 切断するまで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 件のコメント:
コメントを投稿