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

2024年4月17日水曜日

VLC for Androidからユーザ名とパスワードで保護されたSMBプロトコルで共有されたネットワークフォルダにアクセスする方法

 タイトルが長いです。要するにAndroid版のVLC(ver.3.5.4)でIDとパスワードで保護されたWindows10,11,Samba鯖の動画を見ようって話です。

前置きです。

この頃は光回線などで有線LANを引いている多くの方が無線LAN基地局(WIFIルータなど)を設置しておいでと思います。
とても便利ですが、対インターネット経路だけでなく、隣近所や道路からの無線によるアクセスも簡単にできるということでもあります。WPA2-PSKだのなんだので保護されているといっても、故意であるなしにかかわらず、簡便な攻撃経路が一挙に拡大した事実には相違がないわけでして、昔のようにLAN内だからといって匿名ユーザにアクセスを許すような共有をするというのは以前にもまして危険となってきました。

Windows10や11も初期設定ではセキュリティホールが大きすぎてどうにもならなくなってメンテナンスが放棄されたSMB1.0は廃止され、SMB2や3であっても匿名アクセスは原則として無効とされています。

VLC for AndroidはSMB2以上に対応しているのですが、匿名アクセスを前提としたUIになっていてアカウント名とパスワードを指定してアクセスしたいな、というときに、ちょっとややこしいです。で、その手順をメモします、という記事です。

前置きは以上です。

さて、VLCを起動して以下の画面になったとします。

ここでSERVER1はWindows11などでIDとパスワードで保護されたフォルダだったとします。それを選択すると
こんなこと言われます。
ID(アカウント)もパスワードも聞いてくれません。悲しいです。

聞いてくれないので、「お気に入り」としてサーバを指定して対応してあげる必要があります。
最初の画像の左上の三点メニューを押下すると以下のメニューが表示されます。


そして、赤丸で囲った「お気に入りにサーバーを追加」を選択しますと、以下のような画面が表示されますので、それぞれ記入します。
  • プルダウンからSMBを選択します。
  • ポート番号は空欄のまま。
  • ネットワーク共有名は共有フォルダがあるサーバのIPアドレスを。
  • フォルダーパスは空欄のまま。
  • サーバ名は何でもいいです。

まだパスワードは入れていませんが、いったんOKを押して閉じます。

すると、次の位置(緑色の矢印の先)に今作成したserver1が登録されていますので、赤丸で囲った3点メニューを選択して再度編集します。

すると以下の画面が開きますので、「ユーザー名」の欄に、ユーザ名とパスワードをコロンで区切って1行で記述してください。

昭和を髣髴とさせる設定方法ですね。

以上です。

ここまでお読みいただいてありがとうございました。

2024年1月23日火曜日

DOOGEE T30ProでWidevineのレベルがL3に

 表題の件ですが、DOOGEE T30Proを昨年購入して便利に使用していますが、もともと購入したときはL1だったWidevineが年末ごろのワイヤレスアップデートで更新した際にL3になってしまいました。 

ほっとけばいずれ修正されてワイヤレスアップデートで更新されるだろうと高をくくっていたのですが、年を越しても特に動きが見られませんでした。そこで、こちらから動いてみることにしました。

今回は購入したセラーではなく、DOOGEEのサポートに直接連絡を入れてみました。

とりあえず件名を「How to fix T30Pro to Widevine L1?」として、「L3になっちゃったんだけどL1に戻すには何をしたらいいの?」という内容のメールに、何となく必要そうになりそうな情報として、機種名と現在のアップデートバージョン(DOOGEE-T30Pro-NOEEA-Android13.0-20231127_20231127-1654)とIMEIを併記してホームページに掲載されていたGlobal After-sales Supportのメールアドレス(support-service[アットマーク]doogee.cc)あてに送ってみました。

すると、すぐに返信があったので見てみると、「SNを教えてほしい、*#06#でもみられるよ(意訳)」とのことでしたので、折り返してシリアル番号を送信すると、さらに数時間後には「Wifiで繋げてアップデートしてね」とのメールが届き、実際にワイヤレスアップデートを行ってWidevineがL3からL1に戻ったことを確認しました。

とてもフットワークの軽いサポートだったので、感銘を受けました。

余談ですが、まだ全機にアップデートを配信できるほどテストが進んでいないのか、何かライセンス上の問題があるのか事情は分かりませんが、単に古いバージョンに日付だけ差し替えて戻したわけではなく、L3になってしまった際に直されていた部分(バッテリが何によって使われているか表示ができるようになった件)も正しく機能しています。

まあ、私自身は動画を見ないのでWidevineは実は何でもいいんですが、買い替えたときなどに知り合いに譲ったりすることが多く、譲る対象になりがちな人たちがなぜかやたらと動画を見まくっているので、衝動買いする前に念のため。。。そろそろAndroid14な製品も出そうですし。。。

2023年5月22日月曜日

iPlay50 mini 購入と雑感

軽くて安くて1920x1200な酷比魔方というすごく印象に残る名前を名乗っておいでのALLDOCUBEさんのAndroidの8.4インチタブレット、iPlay50miniを2023/5/10に購入して、12日後に届きました。

Bundle1でよかったのですが、買おうとしたらなぜか4万円の定価を提示されてしまったため、カバーとBTキーボードがセットになったBundle3を選んだところ\15,148円で購入できました。この価格はスゴイ!

この激安価格はいつまで続くのでしょう・・・日本国内で発売したときにどうなるのでしょう?

今回買った場所はAliExpressのALLDOCUBE Official Storeで、この記事を書いている現時点でもまだ6割引きセール中です。

技適はT811として令和4年11月11日付でアメリカのMiCOM Labsで相互承認(MRA)による工事設計認証が通っているようです。

なお、USBケーブルだけでなく100~240V,50/60Hz対応で5V2Aを出力する電源も付属していました。プラグ形状はA型でした。

液晶保護フィルムは光沢タイプのフィルムが予め貼られています。その上からさらに社名入りの保護フィルムが貼られていました。別途フィルムを買っておいたのですが、不要でした。

正面から見た本体のデザインはフチが細くてスマートだと思います。ただ、直方体の箱としては辺というかフチというか、角が割と角ばっているので、片手でタブレットをじかに両サイドを掴むようにして持っていると、指が痛くなりますのでケースが欲しくなります。

(角ばり具合です。。。わかりづらくてごめんなさい)

Bundle3のケースは柔らかいプラスチック製で耐久性はなさそうなので、カバーが必要ならば別途用意したほうがよさそうです。

同じくBundle3のBTキーボードは説明書は付属していませんが、どうやらUSB-C経由での充電式のようです。裏面は金属製ですがなかなか薄くて軽く、大きさはタブレット本体より長辺へ3~4cm程度長く、短編に2cm程度長い程度です。

スリープ解除はサイドボタン押下以外だと持ち上げたときのみのようです。指紋認証や顔認証といった甘えは存在しません。せめて画面ダブルタップでのスリープ解除ができればうれしかったのですが、贅沢は言えません。

充電中やメール受信、着信などで点灯するようなLED的な甘えは一切ありません。情報は待ち設けるものではなく発信すべきものだという強い信念なのかも。

インストール済みアプリは謎の中華アプリのようなものは入っておらず、あっさりしています。

最初から仮想メモリが4GBも設定されていて、さらに大きくできるので、実メモリ4GBじゃ足りないアプリを無理して起動しなくちゃいけない人にはいいかもしれません。
逆に私のような読書端末的な使い方をする人にはかえってパフォーマンスが低下する要因になるので切っちゃったほうがいいかもしれません。

液晶画面はそれなりに綺麗だと思います。色味は青くもなく、黄色くもありません。が、官能評価なので人それぞれと思います。画面の明るさはかなり明るくできます。一方、逆にかなり暗くすることもできるので寝ながら読書には便利だと思います。

ナイトモードやダークモードも完備していますが、ダークモードにするとステータスバーにある黒字の文字(日付など)が見えなくなるという微妙な問題があります。その場合はアクセス補助にて強調文字にすると黒字が白枠で縁取られるので見えるようになったりします。

(ダークモード+ナイトーモードで撮影)

軽さの代償というべきでしょうか、電池の容量は4000mAhなので、やっぱり充電回数は多くなります。読書専用であっても長時間読んじゃうとその日のうちに充電が必要になるかと思います。

以上です。

以下余談です。

私の使用目的は寝ながら図書館で借りた本や青空文庫やKindle本の読書で、その用途にばっちり応えてくれたのがMediaPad T2 7.0 Proでした。

現時点でもMediaPad T2 7.0 Proに満足しているのですが、7年も酷使したせいで、さすがに電池が弱ってきて下手をすると一日で充電が必要になってきました。

そこで、新端末に欲しい条件を考えたら以下になりました。

  • \30,000円程度であること(MediaPad T2 7.0 Proを買った値段+インフレ分)。
  • 仰向けで片手でタブレットの後ろ側から両サイドを掴むように持って読みたい。これまで250gだったため、せいぜい300gくらいじゃないと短時間しか読めない。
  • 解像度は複雑な字形の漢字がつぶれて悲しいので1920x1200ドットはほしい。
  • 3840×1644の6.4インチの携帯を使っているけどやたらに細長いので1行が異様に長くなって読みづらいのでアスペクト比は4:3とか16:10とかそんな感じがいい。
  • 音はならなくても全然かまわない。スピーカーがなくたっていい。
  • 無線LANがつながればセルラーはいらない。
  • 動画は見ないので超どうでもいい。
  • センサー類はいらない。
  • カメラはいらない。
  • バッテリ容量は小さくていい。
  • 読書中にわからない語句を調べたり興味が湧いた事項を調べたり検索したりするのでブラウザは快適に使いたい。

特に重量と解像度とアスペクト比を重視しています。

そこで眺めてみると、私が買ったMediaPad T2 7.0 Proが最後の輝きだったようで、ほんとに条件に合うような端末ってないことが分かりました。

価格を度外視するとブラウザの件がなければBOOX系も興味あるのですが、結局DoCoMoのタブレットかiPad miniがよさそうだね~、と思っていましたら、残念なことに新型のDoCoMoのタブレットは6.5万円。その価格を出すならいっそのことiPad miniのほうが絶対満足感高いだろうなあ、と思ったら、今年の末か来年あたりにiPad miniの第七世代が出るという噂があるそうです。

そんなら出るまでMediaPad T2 7.0 Proでいいや。と、いったんは物欲が収まったのですが、それからたまに7~8インチのタブレットに関する情報(7'はないんですけど)をたまに集めるともなく集めるようになりました。

アイリスオーヤマさんのTM082M4N1-Bの\8,000円引きクーポン大盤振る舞いの時は若干迷いましたが、UAUUさんのT30ほどの重量があるので見送ったりしている中で出会ったiPlay50mini。

300gで激安で1920x1200。UAUUさんののP30も気になりますが、ここまで条件に合致しているのに買わない理由が思い浮かびませんでした。

発売日が待ち遠しくて待ち遠しくてついに5/10の16時、緊張して3度確認したら先着100人のうちから外れちゃう体たらくでした。

そんなわけですので、この端末では音声や映像やカメラなど一切使わないのでその点でのレビューはありません。

以上、だれの役にも立たない記事でした。

最後までお読みいただき、誠にありがとうございました。

2018年10月27日土曜日

Android用のブラウザで暗い背景色で長文が読みたい

当blogは副題をネットウラシマと申しまして、浦島太郎感漂う「こんなことも知らんのか」と言われるようなハズカシイ話題を記録するblogです。

そこで、今回のお題は「Android版のブラウザでの長文読書は背景が真っ白で目につらい。」です。
対策は「背景を黒く(暗く)すればいい。」

当たり前すぎます。
が、今時のメジャーブラウザはこれがまったくできない、またはプラグインが必要とされることに衝撃を感じたので記録する次第です。

結論から先に書きますと、(Chrome以外でメジャーと言えるブラウザの存在があるとして)メジャーどころではAndroid版Firefox+アドオンでのみ実現可能でした。

Firefoxで背景色を暗くするアドオンは複数あるのですが、その中ではDark Readerが個人的には最も出来が良いと感じ、採用しました。PC版ChromeおよびSafari用にも同一アドオンがあります。

他はあまり良い評価点を与えられなかったので名を挙げませんが、速度に難あり(白いままの画面が割と長い間、といっても数秒、表示されてしまう)、色に難あり(反転しなくていい文字色や画像も単純にinverseされたり特定の文字色以外は全部白になったり等)、文字数が多くなると急に無反応、スクロールするとスクロールされた領域が白いまま、など、数は多かれど実質的にはとても使えなかったり、ザンネン感が高いものが多かった中、比較的動作が良好だったのが採用の理由です。

加えて、Android版Firefoxのプライベートタブでは、画面上部のタブ名表示部もアドレスバー表示部も暗色表示になるため、その部分を暗色表示にするためだけのアドオンを探さなくても気になりません。
この部分は「設定」->「一般」->「全画面表示でブラウジングする」オプションスイッチを有効にしたうえで、なおかつ、画面を上にある程度スクロールさせないと引っ込まないという大変鬱陶しい仕様ですので、最初からその部分が暗いプライベートタブで読むと手っ取り早いと思います。

実際にやってみた範囲内では以上です。

以下、なんでAndroid用のブラウザの画面を暗くしたいのかを含めた前置きと、なぜ結論で述べた以外のブラウザで実現できないか、などを書き連ねた戯言です。

子供のころから寝る前に本を読まないと寝付けない習慣がついてしまっていて、枕もとの照明で読んでいました。

ところが、スマートフォンが普及して高解像度で文字も美しく表示され、さらに一画面内の文字数も文庫本1ページ程度は軽く表示できてしまうに及び、枕もとに本を積んでおく必要もなくなり、照明をつけなくとも読めるし本より軽いのですぐにスマートフォン、その後はタブレットを用いるようになりました。

読書に用いるアプリ、例えばKindle、自炊jpeg化した小説などではPerfect Viewerなど当然のように白黒反転機能がついています。
Android4.2だか4.3まで標準搭載されていた「(アプリ名としての)ブラウザ」(ややこしいので以後旧標準ブラウザと表記します)にも、実は画面の反転表示機能があります。

尤もブラウザを使って本(長文)を読むことはなかったので、主に白黒反転機能があるアプリだけでこれまで読んできました。

ところが最近、中国古典に再度嵌りそうになっています。
Web上に面白い文献がたくさんあり、すぐに原文を参照出来たり、意味が分からない言葉の解説をすぐに調べることができ、これは寝る前にちょっとづつ読むのにいいな、画面が大きくて複雑な漢字でも明瞭に表示できるタブレットで読みたいなと思いました。

が、現在の主流ブラウザの背景色は白い。とても白い。
暗い中で真っ白な画面を見た瞬間、虹彩を絞って瞳孔を狭める筋肉に痛みが生じるくらい急激な動きを生じる程度に極めて白く、そして眩しいのです。

とてもではありませんが「読む」なんてできません。せいぜい「見る」程度でもすぐにギブアップです。
普段PCで使うモニタよりも段違いに近くで光る白い板を、暗い中で見つめることになるのですから当然といえば当然です。

旧標準ブラウザを搭載するスマホも手元にあるにはあるのですが、XperiaAなので画面が小さいためタブレットで読みたいということで、調べてみました。

さて、ようやく本題です。

まず普段だったら大本命のAndroid版Chrome。
PC版と違い、アドオンも入れられません。だからカスタマイズもできません。
このケースでは絶望です。

旧標準ブラウザもGoogle製にも拘らず、Chromeには旧標準ブラウザのような反転機能が搭載されていないどころか、アドレスバーも何もかも真っ白です。
但し、何の解決にもなりませんが念のために申し添えますと、シークレットタブの場合はアドレスバー表示部だけ暗灰色になりますが表示中のコンテンツの背景色には何の影響も与えません。そりゃそうです。

Android版Chromeを含めて、どうしてもブラウザ自身で表示中のコンテンツの背景色を暗くすることができないとすれば、OSの機能を使って色反転する手があるだろう、と思われるかもしれません。確かAndroid5だか6だかで搭載された機能です。
しかし、当然ながら略図や地図、石碑や鼎などの写真などの画像も反転してしまいます。さらに当然ながらブラウザ以外も反転します。
画面上部の通知領域と下部のナビゲーションバー(戻ったりするボタンがあるバー)も当然ながら色反転されてしまいます。
通常この両者の背景は黒なので、反転すると白くなります。一部だけ輝いていて眩しい上に余計目障りになってしまいます。
じゃあ両者を白くする/できるホームアプリを入れれば解決でしょ、と言われそうですが、そこまでするのは余程そのブラウザに対する忠誠心が高い人に許される特権でしょう。

そこまでして使うほど愛するブラウザなら別ですが、そもそも、単純に不便ですよね。

従って、あくまでもブラウザ内だけで表示コンテンツの背景色およびガワ(アドレスバー等)を暗色で表示できるブラウザを探しました。
以下、それを踏まえて縷々続きます。

お次はAndroid版Edge。インストールしたことがないしWindows10版でも自作javascriptを使った当blog用コンテンツの動作確認のためだけにあるストレージの肥やし感満載のMS期待の星です。
Chrome以外のブラウザが出てくること自体は大歓迎ですので、試しにインストールしてみましたところ、これも真っ白。背景色を暗色にするオプションも見当たらず、アドオンも利用不可。
PC版ではいつかIEのシェアを抜く日が来ることをお祈りいたしつつ結局アンインストール。

今度はAndroid版Opera。わかりやすいところに「ナイトモード」という切り替えスイッチがあるので一瞬「おおおっ」と一気に胸が高鳴りますが、実はガワが暗色に変わるだけでメインのコンテンツには適用外です。
他ブラウザでもシークレットだかプライベートだかのモードで開けば大抵そのあたりは暗色ですのであんまり魅力がありません。
ていうか最近ナイトモードとか流行ですねぇ。
平たく言っちゃえば、単純にブルーライト商法ってやつですかねぇ。
でしょうねぇ(自己納得)。

そしてなによりAndroid版Operaも背景色を暗色にするオプションが見当たらず、アドオン適用不可能のためカスタマイズ不可。
個人的にはPC版Opera12までは相当お世話になっていたため、過去一瞬だけAndroid版Operaを使ったことがあるのですが、その際は様々なサイトがまともに表示されずにレイアウトが盛大に崩れていたので即アンインストールした覚えがあります。
今回も、要件に合致しないためまたもお払い箱にせざるを得ませんでした。

今度はAndroid版Firefoxです。先の結論で申しました通り、Firefoxにアドオンを適用出来たおかげで要件を達成することが出来ました。

完全に個人の環境のせいだと思うのですが、本当はこれは避けたかった。
利用したいタブレットがMediaPad T2 Proで、この端末でFirefoxを使うと、Firefoxの初回のページ読み込みが何かのタイムアウト待ちに引っかかっているらしく、自宅サーバのホームページだろうがgoogleの検索ページだろうが、ともかくなんでもかんでも初回表示に必ず数十秒かかるためです。

同じAndroid版Firefoxでも他のスマートフォンでは全くそんな現象は起きませんし、当然他のブラウザでは一切そういったことは起きないのですが、起動後一度でも読み込みを完了しさえすれば速度的には問題なくなりますし、ページのレイアウト崩れもあまりないというメリットもありますので、Windows版とLinux版ではお世話になっていることもありますし、これからAndroidでもお世話になることにいたしました。

他にもAndroid版ブラウザはあまたありますし、中には反転機能を最初から備えたブラウザもあるのかもしれませんが、とりあえずはこれで運用してみたいと思います。

それにしても、PC上では背景色が黒くないと嫌なのはテキストエディタとvt100エミュレータくらいで、これはMS-DOSのころのアプリは背景色が真っ黒なのが一般的だったなど慣習的なもので、眩しさとかは全く関係がありません。現に、個人で気軽に使えるマルチウィンドウシステムが普及した後で使い始めたIDE(VisualC++とか)やらExcel,Word等の背景が真っ白でも全然違和感を感じません。当blogの背景色も真っ白ケッケです。

部屋が暗い中でPCを使うなんてこともありませんので、背景色を暗くしたいということを切実な要望として感じるのはスマートフォンやタブレットならではといえるのかなあ、と勝手に思う次第です。

以上、恥の記録と長大な駄文でした。
お読みいただき、誠にありがとうございました。

2017年3月16日木曜日

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

前承

今回はAndroid側でPCMデータを受信して再生するアプリを作成します。
といっても、Android側で用意されているライブラリが優れているので、アクティビティ、ソケット接続・受信スレッド、PCM再生スレッドの三本を用意すればそれでおしまいです。
一応、手順を確認します。
  1. connectする
  2. 再生形式を受信してAudioTrackを初期化する
  3. PCMデータを受信する
  4. 切断するまで3に戻る
  5. 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>
2.javaコード
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;
 }
}
3.AndroidManifestはこんなんで。
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も内蔵時計の時間のいい加減さは折り紙付きですので、下手をすると見た目では数千ミリ秒もの遅延が発生してるように見えたりしますが、単純に時計があってないだけです。

まあ、それはそれとして、順調にいくと確かに録音レイテンシ以上の遅延なく再生できます。
但し、通信帯域はそれなりに消費しますし、それ以上に電源を猛烈に食いつぶします。バッテリ駆動下での検証はお勧めしません。

んー。

ここまで聞いたけどだから何、って聞きたいんでしょう?
だからどうしたといわれても。

好きなんです、こういうの。