今回はAndroid側でPCMデータを受信して再生するアプリを作成します。
といっても、Android側で用意されているライブラリが優れているので、アクティビティ、ソケット接続・受信スレッド、PCM再生スレッドの三本を用意すればそれでおしまいです。
一応、手順を確認します。
- connectする
- 再生形式を受信してAudioTrackを初期化する
- PCMデータを受信する
- 切断するまで3に戻る
- 1に戻る
さて、まず、ソケット接続・受信スレッドクラスです。
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 | public class PCMDownloader extends Thread { PlayAudio m_pa; Socket s; boolean m_bIsForceStop = false ; String m_hostaddr = "" ; String m_errstr = "" ; public PCMDownloader( PlayAudio pa ) { m_pa = pa; } int readall( DataInputStream dis, byte [] buf, int len ) throws IOException { int spos = 0 ; int remain = len; int readlen = 0 ; while ( true ){ try { readlen = dis.read( buf, spos, remain ); if ( readlen < 0 ){ // closed return - 1 ; } if ( remain - readlen <= 0 ){ break ; } spos += readlen; remain -= readlen; } catch ( SocketException e ){ if ( m_bIsForceStop == false ){ m_errstr = e.getMessage(); } } catch ( Exception e ){ StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter( sw ); e.printStackTrace( pw ); pw.flush(); String str = sw.toString(); System.out.println( "Exception: " + e + "TRACE:" + str ); m_errstr = str; return - 1 ; } } return 0 ; } static int byte2DWORD( byte [] bytes, int startpos ) { return ( ( ( short )bytes[ startpos ] ) & 0xFF ) << 24 | ( ( ( short )bytes[ startpos + 1 ] ) & 0xFF ) << 16 | ( ( ( short )bytes[ startpos + 2 ] ) & 0xFF ) << 8 | ( ( ( short )bytes[ startpos + 3 ] ) & 0xFF ); } static int byte2WORD( byte [] bytes, int startpos ) { return ( ( ( short )bytes[ startpos ] ) & 0xFF ) << 8 | ( ( ( short )bytes[ startpos + 1 ] ) & 0xFF ); } public void recv( Socket s ) throws IOException { byte [] recvbuf = new byte [ 1024 * 1024 ]; InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream( is ); while ( true ){ int ret = readall( dis, recvbuf, 24 ); if ( ret < 0 || recvbuf[ 0 ] != 'F' || recvbuf[ 1 ] != '*' || recvbuf[ 2 ] != 'C' || recvbuf[ 3 ] != 'K' ){ dis.close(); s.close(); break ; } Calendar c = SystemTime2Calendar( recvbuf, 4 ); // JAVA大嫌い。バカみたいなコード書かされる.byte型のくせに符号付だよ馬鹿か int wavdatasize = byte2DWORD( recvbuf, 20 ); if ( readall( dis, recvbuf, wavdatasize ) < 0 ){ dis.close(); s.close(); break ; } m_pa.addData( c, recvbuf, wavdatasize ); } } static Calendar SystemTime2Calendar( byte [] bytes, int startpos ) { Calendar c = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); c.clear(); c.set( byte2WORD( bytes, startpos ), byte2WORD( bytes, startpos + 2 ) - 1 , byte2WORD( bytes, startpos + 6 ), byte2WORD( bytes, startpos + 8 ), byte2WORD( bytes, startpos + 10 ), byte2WORD( bytes, startpos + 12 ) ); c.set( Calendar.MILLISECOND, byte2WORD( bytes, startpos + 14 ) ); return c; } public void run() { init(); loop(); } public void setHostAddr( String s ) { m_hostaddr = s; } public void forcestop() { try { m_bIsForceStop = true ; s.close(); } catch ( Exception e ){ // 無視 } m_errstr = "disconnected" ; } public boolean recvWaveData( Socket s ) throws IOException { InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream( is ); byte [] recvbuf = new byte [ 12 ]; int ret = readall( dis, recvbuf, 12 ); boolean bret = false ; if ( ret == 0 && recvbuf[ 0 ] == 'f' && recvbuf[ 1 ] == '*' && recvbuf[ 2 ] == 'c' && recvbuf[ 3 ] == 'k' ){ int wBitsPerSample; int nChannels; int nSamplesPerSec; wBitsPerSample = byte2WORD( recvbuf, 4 ); nChannels = byte2WORD( recvbuf, 6 ); nSamplesPerSec = byte2DWORD( recvbuf, 8 ); m_pa.init( wBitsPerSample, nChannels, nSamplesPerSec ); bret = true ; } return bret; } public void loop() { while ( m_bIsForceStop == false ){ try { int port = 1234 ; s = new Socket( m_hostaddr, port ); m_errstr = "connected to " + m_hostaddr + ":" + port; if ( recvWaveData( s ) == false ){ s.close(); continue ; } recv( s ); } catch ( Exception e ){ // System.out.println("Exception: " + e + e.getStackTrace()); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter( sw ); e.printStackTrace( pw ); pw.flush(); String str = sw.toString(); m_errstr = str; System.out.println( "Exception: " + e + "TRACE:" + str ); } } } void init() { m_errstr = "" ; m_bIsForceStop = false ; } } |
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 | public class PlayAudio extends Thread { class PlayData { byte [] buf; int len; long millisec; } public long m_latency; private AudioTrack mAudioTrack = null ; List<playdata> m_list; synchronized int getListCount() { return m_list == null ? 0 : m_list.size(); } synchronized void addData( Calendar c, byte [] buf, int len ) { PlayData pd = new PlayData(); pd.buf = new byte [ len ]; System.arraycopy( buf, 0 , pd.buf, 0 , len ); pd.len = len; if ( m_list == null ){ m_list = new ArrayList<playdata>(); } pd.millisec = c.getTimeInMillis(); m_list.add( pd ); notify(); } synchronized List<playdata> getAllData() throws InterruptedException { wait(); List<playdata> retlist = m_list; m_list = null ; return retlist; } synchronized PlayData getData() throws InterruptedException { if ( m_list == null || m_list.isEmpty() == true ){ wait(); } PlayData pd = m_list.get( 0 ); m_latency = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ) .getTimeInMillis() - pd.millisec; m_list.remove( 0 ); return pd; } synchronized void clearData() { if ( m_list != null ){ if ( m_list.isEmpty() == false ){ m_list.clear(); } m_list = null ; } } public void run() { init( 16 , 2 , 48000 ); try { loop(); } catch ( InterruptedException e ){ // ignore } catch ( Exception e ){ StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter( sw ); e.printStackTrace( pw ); pw.flush(); String str = sw.toString(); System.out.println( "Exception: " + e + "TRACE:" + str ); } } void loop() throws InterruptedException { while ( true ){ PlayData pd = getData(); mAudioTrack.write( pd.buf, 0 , pd.len ); } } public void init( int wBitsPerSample, int nChannels, int nSamplesPerSec ) { clearData(); int bit; switch ( wBitsPerSample ){ case 8 : bit = AudioFormat.ENCODING_PCM_8BIT; break ; default : bit = AudioFormat.ENCODING_PCM_16BIT; break ; } int ch = AudioFormat.CHANNEL_OUT_STEREO; switch ( nChannels ){ case 1 : ch = AudioFormat.CHANNEL_OUT_MONO; break ; case 2 : ch = AudioFormat.CHANNEL_OUT_STEREO; break ; } int minBufferSizeInBytes = AudioTrack.getMinBufferSize( nSamplesPerSec, ch, bit ); try { if ( mAudioTrack != null ){ mAudioTrack.stop(); mAudioTrack.flush(); } } catch ( Exception e ){ // ignore } mAudioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, nSamplesPerSec, ch, bit, minBufferSizeInBytes, AudioTrack.MODE_STREAM ); mAudioTrack.play(); } } |
AudioTrackクラスが優秀すぎてほとんどやることがありません。
Activityはこんな感じで。
1.レイアウト
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 | < RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = "${relativePackage}.${activityClass}" > < TextView android:id = "@+id/textView1" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "@string/hello_world" /> < TextView android:id = "@+id/textView_Queue" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignLeft = "@+id/button1" android:layout_below = "@+id/textView1" android:layout_marginTop = "102dp" android:text = "Medium Text" android:textAppearance = "?android:attr/textAppearanceMedium" /> < Button android:id = "@+id/button1" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentLeft = "true" android:layout_below = "@+id/textView_Queue" android:layout_marginLeft = "40dp" android:layout_marginTop = "39dp" android:onClick = "onClickTestButton" android:text = "Button" /> < EditText android:id = "@+id/editTextHost" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentLeft = "true" android:layout_below = "@+id/textView1" android:layout_marginLeft = "27dp" android:layout_marginTop = "39dp" android:ems = "10" android:text = "pine" /> < TextView android:id = "@+id/textViewErrStr" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignLeft = "@+id/button1" android:layout_below = "@+id/button1" android:layout_marginTop = "41dp" android:text = "input hostname and run" /> </ RelativeLayout > |
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 | public class ActivityMain extends Activity { PCMDownloader m_pdl; PlayAudio m_pa; Button testButton; TextView textView_queue; TextView textView_errStr; EditText editText_hostAddr; boolean testButtonState = false ; Handler handler = new Handler(); Runnable runnable = new Runnable() { public void run() { try { textView_queue.setText( "Queue: " + m_pa.getListCount() + " Latency:" + m_pa.m_latency + "ms" ); if ( m_pdl != null ){ textView_errStr.setText( m_pdl.m_errstr ); } } catch ( Exception e ){ // 無視 } handler.postDelayed( runnable, 1000 ); } }; @Override protected void onCreate( Bundle savedInstanceState ) { super .onCreate( savedInstanceState ); setContentView( R.layout.activity_main ); testButton = (Button) this .findViewById( R.id.button1 ); testButton.setText( "run" ); textView_queue = (TextView) this .findViewById( R.id.textView_Queue ); textView_queue.setText( "Queue:0 ..." ); textView_errStr = (TextView) this .findViewById( R.id.textViewErrStr ); editText_hostAddr = (EditText) this .findViewById( R.id.editTextHost ); m_pa = new PlayAudio(); m_pa.start(); runnable.run(); } public void onClickTestButton( View view ) { if ( testButtonState == false ){ m_pdl = new PCMDownloader( m_pa ); String addr = ( (SpannableStringBuilder)editText_hostAddr.getText() ) .toString(); m_pdl.setHostAddr( addr ); m_pdl.start(); testButton.setText( "stop" ); } else { m_pdl.forcestop(); testButton.setText( "run" ); } testButtonState = !testButtonState; } } |
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 | <?xml version= "1.0" encoding= "utf-8" ?> <manifest xmlns:android= "http://schemas.android.com/apk/res/android" package = "org.pinefield.pcmreceiver" android:versionCode= "1" android:versionName= "1.0" > <uses-permission android:name= "android.permission.INTERNET" /> <uses-permission android:name= "android.permission.ACCESS_NETWORK_STATE" /> <uses-sdk android:minSdkVersion= "17" android:targetSdkVersion= "17" /> <application android:allowBackup= "true" android:icon= "@drawable/ic_launcher" android:label= "@string/app_name" android:theme= "@style/AppTheme" > <activity android:name= ".ActivityMain" android:label= "@string/app_name" > <intent-filter> <action android:name= "android.intent.action.MAIN" /> <category android:name= "android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |
うーんと。
ええっと、わざと手を抜いているところがあって、Android側で重い処理やらが始まるとみるみる滞留した未再生データがたまるのがActivityから確認できます。
これをわざと音飛びさせてすっ飛ばす処理を最初は入れていたのですが、切断して再接続すれば済む話なので検証用にすっ飛ばす処理を抜きました。
滞留するときはものすごい勢いで溜まります。
また、2ch以下で16bitPCMデータしか再生できないコードになっています。
レイテンシの計算ですが、AndroidもPCも内蔵時計の時間のいい加減さは折り紙付きですので、下手をすると見た目では数千ミリ秒もの遅延が発生してるように見えたりしますが、単純に時計があってないだけです。
まあ、それはそれとして、順調にいくと確かに録音レイテンシ以上の遅延なく再生できます。
但し、通信帯域はそれなりに消費しますし、それ以上に電源を猛烈に食いつぶします。バッテリ駆動下での検証はお勧めしません。
んー。
ここまで聞いたけどだから何、って聞きたいんでしょう?
だからどうしたといわれても。
好きなんです、こういうの。
0 件のコメント:
コメントを投稿