ラベル java の投稿を表示しています。 すべての投稿を表示
ラベル java の投稿を表示しています。 すべての投稿を表示

2021年11月20日土曜日

GitBucketのH2データベースがまた壊れた

便利に使わせていただいております。

批判だなんてとんでもないこってす。そんな意図は毛頭ございませんので念のため。

私が悪いんです。

前置きです。

久しぶりにGitBucketを更新する衝動にかられました。

4.31.2から4.36.2です。

過去の例から絶対何か起きるな、という確信がありました。

だから、全然ちっとも悔しくなんかないんだもん。


今回は4.36.2にwarを置き換えたとたんにこんな感じです。

19:12:49.124 [main] ERROR com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Exception during pool initialization.

org.h2.jdbc.JdbcSQLException: テーブル "REPOSITORY" が見つかりません

で。4.31.2に戻しても

Caused by: 

com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to initialize pool: テーブル "REPOSITORY" が見つかりません

だそうで。

4.36.2がテーブルをぶっ壊してくれるみたいですね。
そのため、4.31.2に戻しても後の祭りと。

すげえなあ。後方互換などには目もくれぬ未来しか見つめない設計思想。まさに聖帝サウザーばりですね。「ひかぬ。媚びぬ。顧みぬ。」でしたっけ。

多分4.31.2からリリース順にリプレース/起動/停止を繰り返してアップグレードすれば問題ないんでしょうね。思いついたときにドーンと更新した私が悪うございます。

この事例は以前もありましたので、もうこれ使うのやめたいなあと思っていながら惰性で使い続けたのも自業自得。

使うのをやめるつもりだったので、GitBucket専用の設定ファイル類のバックアップはとってませんでした。これも自業自得です。

ところで、タイトルにはH2データベースが壊れた、としましたが、この様子だと、GitBucketさんはH2データベースを信頼性がない云々で目の敵にしておいでですが、H2だから壊れたんじゃなくてGitBucketさん自身がぶっ壊しているようにしか見えません。
まあ、私が確認した範囲内ではH2データベースが壊れたのでタイトルはそのままにしておきます。


まあ、さっき新規プロジェクトをコミットしたばっかなので、その辺復旧できるなら復旧したいなあということで、試みてみました。
この際、懸案だったGitBucketから別のシステムに移行する手間か、単純にこの場は復旧だけしておいて済ませるか、多少考えないではありませんでしたが、結局やっつけで済ませられるほうを選びました。

まず、H2データベースのダンプを試みます。

H2公式から1.4.200をダウンロードしてみたところ、ファイルポインタがどうのこうのでさっぱり使えません。

何を言ってるのかわからないので早速検索をしたところ、199を使わないとダメなんだそうで。

なんなんだそろいもそろってjava業界って・・・未来しか見えてないのね。

ちゃっちゃとおわらそう。

java -cp h2-1.4.199.jar org.h2.tools.Shell -url  jdbc:h2:file:data -user sa -password sa

で中身をのぞこうとしても

「org.h2.jdbc.JdbcSQLException: テーブル "REPOSITORY" が見つかりません」

この段階でDB全体の整合性が整っていることが必要なのね。

ウームと唸って、さらに探すとRecoverというツールがあるらしいので助かりました。
カレントディレクトリにdata.mv.dbがある前提で

java -cp h2-1.4.199.jar org.h2.tools.Recover

と実行すると、data.h2.sql とdata.h2.txtという2ファイルが生成されました。

そこで、data.h2.sql をさっそくのぞいてみると、O_??(数値)というあからさまにテンポラリな名前のテーブルがいくつか作られていました。

そして、そのテーブルに対してINSERTが発行されています。

さて、このO_で始まるテーブルは何なのかな?と思って、よくよく調べてみると、スクリプトの後半で本来のテーブルと思しきテーブルにinsert from selectしてました。

なるほど、制約条件の整合性対策なんだろうな、と納得した次第です。

で、確かに、テーブル "REPOSITORY"はありません。

その代わり、REPOSITORY_COPY_3_3っちゅーのが見つかりました。

なーんだかよくわかんないですけど、こーゆー名前のつけかたって、多分、ver3.3のころにアップグレード処理を入れた名残なんでしょうかね。

あれ?でも、これ、insert文にさっきgitにcommitしたばかりのはずのリポジトリ名も入ってる・・・

わけわかんないけど、これに対してinsertしているのをテーブル "REPOSITORY"に向けなおしときました。

ついでに、カラムが追加されてるテーブルがあり、insert文が通りませんでした。
そこで、4.36.2で新規作成した空のデータベースファイルをダンプして比較したところ、create table文にカラムが追加されており、default trueとか書いてあったのでinsert文の該当カラムにtrueを追記しておきました。

そして、それらテーブル名とカラムの追加を行ったinsert文をh2-1.4.199.jarではなくGitBucketの管理コンソールの「Database viewer」なる機能から流し込んでみたところ、見た目は復旧出来たっぽいのでもう終わりにします。

H2公式ツールを使わなかった理由は、二つあります。

いくら公式だからってh2-1.4.199で読めてh2-1.4.200で読めなくなるようないい加減なツールなんかおっかなくて使いたくないし、GitBucket自身が何を使ってんのか知らないのでこのプログラム自身が使ってるライブラリでDBをいじらないとまた七面倒なことが起きそうだと思った次第です

ま。こんな感じです。

更に次回、やっぱり惰性で使い続けて似たようなことが起きた場合に備えて忘備録としたいと思い記録しました。なお、cronでバックアップする元をリポジトリ以外にGitBucketの設定定義体も念のため加えておきました。GitBucketを廃止したらこれを忘れずに除去しなくてはならないのが今からおっくうです。

こんな戯言にお付き合いいただき、誠にありがとうございました。

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側は次回とさせていただきます。

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

本当にくだらないことをやってみましたので、顛末を記録して長く恥をさらすことにしました。

自分で操作中のPCの音をAndroidに飛ばしたい。無線LANで。なるべく遅滞なく。

今回はすべて余談(というかffmpeg利用編)です。

さて、なぜこのようなことをしようと思ったのかはクSonyのBluetoothレシーバです。
有機ELディスプレイがはるか昔に死んでてペアリングがだるいのです。
画面が見えないから。

画像付きでいいならリモートデスクトップがあるので意味がございません。
ただ、リモートデスクトップだと、同一ユーザでPCを操作しながら音声だけ別計算機から出力するというわけにも参りませんので今回の要件としてはパスです。

こないだ連打病から帰ってきた(ついでに基盤交換までされてしまって再ペアリングが必要になってしまっていた)XperiaAに見えないままどうにか再ペアリングを果たしたので、こいつがペアリングされているAndorid機にPCの音声を飛ばせばBluetooth経由で再ペアリングなしで使いまわせるぞ!という目論見です。

なんという恥知らずな目論見でしょうか。
レシーバなんか数千円なんだからもう一つ買えばいいのに、と理性は申します。

ですが、使えるとなると骨の髄まで啜ってやるのが供養というものではないかと情に流されてしまうのです。

などと意味不明な言い訳をしつつ(ケチ)、まず試したのは既存のアプリを使用してPCの音声をAndroidに送って再生させるというものです。

この試みに選択したのはffmpeg+vlcの組み合わせです。
ffmpegはPC上の音をミキサを経由してDirectShow(以下ffmpeg方言式にdshowと表記します)で録音して、それをtcp,udp,rtpなど様々な形式で単体で送信することができます。
vlcはそれらすべてを受信することができますし、加えて、Androidはもちろん、様々なOSにポーティングされているのでOS間の比較・検証がやりやすいのでやってみました。

まず何も考えずにdshow経由でvlcにudpのポート番号1234にマルチキャスト送信するコマンドは次の通りです。
ffmpeg  -f dshow -i audio="ステレオ ミキサー”  -f mpegts  udp://224.0.0.1:1234
なお、入力元(-i audio=)に使えるdshowデバイスの一覧は次のコマンドで表示することができます。表示されたお使いの計算機の "DirectShow audio devices" の欄からチョイスしてください。
ffmpeg -f dshow -list_devices true -i NUL
この例ではブロードキャストアドレスをサンプルにしていますがユニキャストでもどっちも同じで、遅延は5秒以上、しかも(マルチキャストなので同時受信していた別PCでは取りこぼしはなかったのですが)Android相手だと2台ある両者とも音の取りこぼしがひどいありさまでとても使い物になりませんでした。

rtpも同様です。rtpもudpに乗っかってるのが原因と思われます。まあ、udpってそういうもんですからいいっちゃいいんですが、近所に誰も基地局を設置していない5GHz帯でもこのありさまなのは少々失望の念を禁じえません。

vlc側の設定はudpの場合のみudp://@224.0.0.1:1234のようにアットマークが必要なのでご留意ください(ユニキャストも同様です。ユニキャストの場合は送信元のffmpegに指定するuriは送信先計算機宛、vlcで指定するのは自計算機のアドレスとなります)。

ま、さすがにudpやrtpはudpで送ってるんだからパケットがetherの藻屑に消えるのはプロトコル上の仕様ですから、今度はtcpでやってみます。

tcpはマルチキャストという芸当が使えませんから(当たり前だわな)一対一で通信しますのでどっちかが待ち受けて、どっちかが接続しにいくことになります。
Android側で待ち受けさせてからPC側で接続させるというのも運用上極めて問題があることは自明なので、PC側で待ち受けして、Android側が気が向いたときに接続してくるようにできるとすると、次のようなコマンドになります。
ffmpeg -f dshow -i audio="ステレオ ミキサー" -f mpegts tcp://192.168.0.1:1234?listen
この場合の192.168.0.1というのが送信元のPC機のIPアドレスです。uriの後ろに?listenがつくのが味噌です。
vlc側はtcp://192.168.0.1:1234と指定すると、ffmpegに接続に来ます。?listenを抜けば逆になります。

これだと、確かに音の抜けがなくなりましたが、やはり遅延がすごい。すごいけど、音飛びがなくなったので遅延のほうに目を向けてみたいと思います。

やはり、仕組み上どうしようもない要件としてdshow経由だと
再生デバイス鳴動->録音->tcp|udp送信
という手順を踏むことになり、特に録音段階で録音時間を100msとするなら100ms、確実に遅延します。なんせ録音してるんですからその間どうしようもない。

ですが、5~7秒とか現状ではいくらなんでもおかしいので、ffmpegのオプションを見直してみてみますと、以下のようになりました。
ffmpeg  -f dshow -i audio="ステレオ ミキサー"  -audio_buffer_size 10 -probesize 32 -analyzeduration 0 -start_time_realtime -10 -f mpegts tcp://224.0.0.1:1234?listen
ffmpegはエンコーディング目的なソフトなのでなるべく読み込んでから一気に処理して結果を効率よく出そう、という設計方針なので、それに敢えて逆らってとにかくバッファリングを極力減らしてみました。

各オプションの詳細はffmpegのマニュアルをご覧いただくとして、udpだと遅延が1秒未満と、結構いい線いくのですが、Android機のudp受信状況が思わしくない。
結局tcpにせざるを得ないわけですが、どういうわけかtcpのほうが遅延時間が伸びるのです。

いろいろ試してみると、どうもffmpegはconnect前のデータから順に送信しようとするらしく、接続した時点からの音声を送るということができないようで、それが遅延に見えるという事のようでした。
確かにffmpegの準備が整ったらすかさずvlcから接続すると遅延時間が短くなります。

・・・なるほど。

こうなったら自前でキャプチャした音声をandroidに送って再生させたらどうなるんだろう?!

どうでもいい深みに入ってまいりました。
次回から本題です。

2013年12月31日火曜日

google app engine で文字化け(split)

自作の逆ジオコーディングサービスをgoogle様にホスティングしていただいていて、それをjavaでもって実装していた。

ちょっと考えがあって実装してみたら実に秒からミリ秒単位への高速化を実現。
最初からやっとけよ!っていう内容なんですけどね。
無料枠と有料枠の端境でDataStoreとInstanceのバランスをとることもうまくいけそうだなあ、なんて年末にホクホク。

で、デプロイしたらとたんに文字化け。12/31早朝に。
データもソースも全部UTF8で固めてあっても。
イヤになるねぇ。Latin1圏がうらやましい。

結論はString.split()。
こいつのソース読んでないけど、これを行うと文字化けする。

やっぱ他人が作ったライブラリはわかんないね。
オブジェクト原理主義者の犯行だな!

ていうかいいよ別にバイト列で・・・
オブジェクト指向だ!!!!!!!とか、肩に力入りすぎ。この言語。

HttpURLConnectionが親切すぎて生きるのがつらい

誰なんですかねえ、こんなオレオレライブラリっぽいのマージしたのは。
自分のアプリだけで使っとけよ!というくらい親切すぎて生きるのがつらい。

getResponceCode()だけでブロックする、というよりread()でブロックする。
ブロックそのものは文句はないんだけど、せめてデータが1バイトでも届いているかチェックするためにinputstreamを取得するだけでブロックする。そこまで面倒を見て下さるなんて感激。
setReadTimeoutが、レスポンスが返ってくるまでの間という大くくりにしか適用できない。
サーバのレスポンスが一定以上だとそもそもreadできない。例外がおきる。

オレオレライブラリの作者には200以外いらなかったのはわかるが、よりによってFileがないよ、っていう例外放り投げるなんてたまげたなあ。超イケテル。

そもそも使うなという評価しか思い浮かばない。

確かにイチからhttpのような原始的なプロトコルの実装を今更やんのかよ、と言いたくなる気持ちもよくわかるけど、こういうオレオレライブラリはSDKから外したほうがいい。

シャリンノサイセイサンガーとかいう人は良く考えたほうがいい。
車輪は回るから車輪であって回らないのは車輪ではない。
無理に回す必要はない。よく回る車輪を作ればいい。

まあ、いろいろ多環境下における仮想環境がらみとかネットに強いアピールが必要だった当時の政治的背景とかあるんでしょうけど。
でもcommitした人に聞いてみたいね。
どう思う?って。

最初からselect()導入しとけばいいのに・・・
当時はなかったとしても、せめてThread.interrupt()できれば使い勝手は向上するとは思うけど、
いったい、だれがこんなもんSDKに忍ばせたんだろう
裏のスレッドでdisconnect()すればキャンセルできるよ!とかへぼすぎでしょ。。。

かくして車輪の再生産が始まるのであった。