Jake's BLOG

最近のIT技術動向と社会問題など・・・・

YAMAHAハイエンドAVアンプ RX-A3060と中華デジアン Indeed TDA7498E 勝負

私の最終目的地であるYAMAHAのハイエンドAVアンプを入手。この機種自体のレビューは各所にあるので割愛します。YAMAHAの多彩なDSPや採用されているESSのDACなどに関して疑いはありません。特に「YPAOボリューム」なる機能は、実のところはただのイコライザーなのですが、YPAOの自動計測によって、スピーカーごとに、実測値を元に設定され、試聴環境や、スピーカー自身の特性でゆがむ周波数帯域ごとの強弱を逆にかけて補正します。スピーカー配置と部屋が左右対称でなければ、当然、この「イコライザー」のかかりも左右異なった設定になるのですが、これはリスニングポジションで均等に聞こえるようにするための補正です。この機種にして一番衝撃だったのはこの機能でした。

一方で気になるのは、メインアンプ部分の性能です。俗には、AVアンプは単体のプリメインアンプには絶対にかなわないといわれています。これまで、つなぎとして使ってきたデジタルアンプIndeed TDA7498Eを使って比較してみようと思います。

比較試聴条件は、

1. YAMAHA RX-A3060 → DALI IKON6 MK2
2. YAMAHA RX-A3060 (BI-AMP)→ DALI IKON6 MK2
3. YAMAHA RX-A3060 (PREOUT) → Indeed TDA7498E → DALI IKON6 MK2

となります。

音源は、CD相当のFLACのクラシック、Google Play Music(MP3 320kbps相当)で、3060はいわば、ネットワークプレーヤー・DACとして働いてもらいます。アンプそのものの比較のためにピュアダイレクトモードで聞きます。

1と2の比較、以前 RX-V2067 を使ったときはBI-AMPとシングルの場合とで大きな差がありましたが、今回はほとんど差がわかりません。1chのアンプの出力が十分大きいことが効いているのだと思います。ATMOSなどで多チャンネルを楽しみたい場合や、スピーカーの数でチャンネルがあえて余っているということでなければ、BI-AMP構成にする意味はあまりありません。

気になる2と3の聞き比べに驚きがありました。

俗にAVアンプのパワーアンプとしての性能は、プリメイン機と比べて、値段をチャンネル数で割った程度といわれていますので、30万円の3060でいうとだいたい6万円前後のプリメイン機相当とざっくり見積もれます。一方で、中華アンプは超小型で省電力のデジタルアンプ1万円強です。YAMAHAの最高級AVアンプが、まさかこれに負けるわけはあるまいと思いながら試聴しました。が、結果、Indeed TDA7498Eの勝ちでした。

聞き比べてまずわかるのが、左右の分離と定位感で、Indeed 7498Eを通すと明らかに、ボーカルがより中央に寄り、オーケストラの各楽器も位置がはっきりしてきます。Indeed恐るべし。次に、パワーですが、POPSやJAZZではなかなか差がわかりにくいですが、オーケストラでたくさんの楽器が一度にバーッとなっている時などに差が出ます。音階を降りていくコントラバスの音が、定位とアタックをキープしながら移動するのがわかりますが、3060単体では他の楽器に埋もれて、アタックが弱く、だれてしまって聞こえます。デジタルアンプ恐るべし。中高域は7498Eのほうがデジタルアンプよろしく、ややキラキラ気味に感じますが、両者で優劣があるほどではありません。むしろこの程度の差は、YPAOボリュームを使うと吸収されてしまうでしょう。

かつてRX-V2067にIndeed TDA7498Eを接続した時の音と比べてはるかに向上しているのは、AVアンプのDACとしての音質が向上しているためで、7498Eの潜在力がまだまだ残っていたことになります。

奇しくも、YAMAHA最高峰のAVアンプのパワーアンプ部分が1万円の中華アンプに負けてしまいましたので、当分は、RX-A3060に外部メインアンプとしてIndeed TDA7498Eを使用する構成で行こうと思います。拙作の12Vトリガー連動タップはここでも活躍です。

TDA7498Eというチップセットを使った人気アンプには SMSL SA-98EやFX-AUDIO- FX1002A/Jなどがありますが、おそらく、この2機種だったすれば、私は外部メインアンプとして使用しなかったと思います。経験的にこの2機種は、Indeedと比べて左右の分離が悪いので、この点でもってRX-A3060の内臓パワーアンプを置き換えるほどとは思わなかったかもしれません。

唯一Indeed TDA7498Eの欠点は、同チップセットの他製品と同じく、無音時でもかすかなホワイトノイズ(ヒスノイズ?)が出ることです。ノイズのレベルはボリュームによらず一定で、24Vと36Vの電源を使って感じたところでは、電源電圧が上がるとノイズレベルも上がるといったところです。スピーカーの構成、試聴距離で、このノイズが聞こえてしまう環境だとつらいかもしれませんが、少なくとも私の環境の場合は、このノイズは、無視できるレベルです。

中華アンプIndeed TDA7498Eは安価なAVアンプやネットワークプレーヤー製品あるいは、単体のDAC製品と組み合わせて限りなく安く、それでいてピュアに近い高音質を実現する追加パーツとして非常に強力だということがわかりました。

中華デジタルアンプおそるべし(2) Indeed TDA7498E編

TDA7498Eというデジタルアンプ用のICを使った中華アンプには有名どころが3つあります。


 FX-AUDIO- FX1002A (NFJ版FX1002J)
 SMSL SA-98E
 Indeed TDA7498E



前者2つは日本でも人気で、オークションなどでも盛んに取引されています。Indeedの製品は、日本ではほとんどレビューがなく、Amazonでも、オークションでも取り扱いがなく、謎に満ちた一品です。

Indeedが直販しているebayの製品ページ

http://www.ebay.com/itm/NEW2014-Indeed-Audiophile-Quality-Class-D-TDA7498E-160WX2-Stereo-Amplifier-Black-/291079772133

では、ユーザーの声を掲載しています。そのほかのフォーラムでも、同じICチップを使っている他社製品とくらべて「すごい!」という書き込みが見られました。

スカスカの回路

製品の回路基板の画像を見て驚きます。こんなにスカスカでいいの?同じアンプICを使用したFX1002Jと比べると部品点数と密集度が全然違います。ただ、素人考えとして、余計な部品が少ない分だけ、アンプのICの素の性能を色づけなく素直に実装しているのだろうなと思うわけです。

綺麗なシンメトリ

回路のデザインがきれいに対称になっています。アンプICは1つですが、そこから出た音が左右きれいに分かれているのがわかります。左右の分離、定位感に強いのではないかと想像します。

オペアンプがない

FX1002Jはオペアンプを2基つんでおり、交換することで音質の変化が楽しめたりするわけですが、実は音にいろいろな化粧をしているようなもので、スピーカー環境や自分の耳にあったチューニングができるという一方で、原音の忠実度という点では離れてしまうように思います。このIndeedの回路のシンプルさはアンプのICが持つ性能を色づけせずにそのまま発揮してくれるのではという期待を持たせます。(アンプICから出たところに対称に回路が構成されていますがこれがディスクリートオペアンプなのかどうかは小生判別できず)

2本のコンデンサがでかい

FX1002Jにも大きなコンデンサ3300μFが積んでありますが、それを超える大きさ8200μFです。なんとなく、力の余裕を感じます。一方でSA-98Eは大きなコンデンサが1つ、あくまで素人の印象ですが、ここで左右の音が混ざるんじゃない?と勘繰るわけです。

実体験

予想通りの高音質でした。結論を先に言えば、TDA7498Eのほかの製品を頭一つ抜きん出ています。特徴的なのは低音がやや控えめであるが自然で、ベースラインが音階を下がっていくときも膨らんだ感じにならずフラットであるように(あくまで私のスピーカー越しで)感じます。高音域は、ギターやバイオリンの金属弦の摩擦音がリアルに、声の破擦音、場の空気感なども聞こえやすくなっています。当初、俗に言われるデジタルアンプの特性よろしく高域が持ち上がっている印象でしたが、1日ほどエージング(バーンイン)が進むとおとなしくなりました。

一番大きな収穫は、左右の定位感が増したことで、回路基板のデザインから想像していた通りの結果がありました。オーケストラでたくさんの楽器が一斉に鳴っている時でもどの楽器の奏者がどこに座っているかさらによく分かるようになりました。

また音の粒状感というかレスポンスというか制動感、例えば、バスドラのヒットが切れの良い「ドッ!ドッ!」と、より乾いた音に聞こえます。他機種では大げさに言えば「ボン!ボン!」と聞こえていたものです。

電源ONのままDC電源が落ちてもポップノイズが出ない!

最後に気が付いた重要な違いです。

FX1002Jでは、アンプの電源スイッチは、内部のリレー回路と連動していて「ブツッ」という音がスピーカーに出ないように作られています。しかし、アンプのスイッチがオンのまま、ACアダプタの電源供給がオフになると、DC出力の電圧が緩やかに数秒かけて下がるのですが、この時アンプ側は電源の落としどころがわからずポップノイズ防止リレーは働かず(間に合わず)、スピーカーに2~3Vのポップノイズが「ブツッ」と流れてしまうのです。

ところがIndeedのこの機種には、リレーらしきものがなく、電源のオン・オフでも「カチッ」という音がありません。それでいて、アンプ側の電源がオンのまま、ACアダプタの出力が落ちた場合でもスピーカーからブツッという音がほとんど(かすかには聞こえる)ないのです。どうやってそうなっているのかわかりませんが、うれしいおまけでした。

TDA7498E 3兄弟の比較

まったくの素人主観による評価ですが、3機種を比較してみました。

 

Indeed TDA7498E

FX-AUDIO-(NFJ) FX1002J

SAML SA-98E

パワー

★★★

★★★

★★★

低音域

★★★(並み)

★★★(やや過剰)

★★★(やや過剰)

中音域

★★★

★★★

★★★

高音域

★★★

★★★

★★★

左右の分解

★★★

★★

音の解像度

★★★

★★

★★

総合

★★★

★★


中華デジタルアンプおそるべし(1) FX-AUDIO- FX1002J 編

5年前のYAMAHAのAVアンプをはるかに超える


オーディオマニアというにはおこがましい程度のオーディオ好きなのですが、5年前にYAMAHAのRX-V2067というAVアンプを購入して以来特段アップグレードもせず長年来ました。当時のYAMAHAのAVアンプでは3067に次ぐ上から2つ目のクラスの機種でしたからそんなに悪くなかろうと勝手に思い込んできたわけですが、最近再びピュアオーディオに開眼することになり、良質の音に対する欲がむくむくと芽生えてきました。

RX-V2067という機種は、100W級のマルチチャンネルのAVアンプで、DolbyやDTSなどのポピュラーなデコーダーを積んで、DLNAなどネットワーク再生にも対応し、当時も今もマルチタレントなAVアンプです。しかし、昨今のハイレゾブームでハイレゾの音源を再生してもなかなかその良さが実感できずにいたところ、さらに上を目指せる余地があることがわかり、いろいろな設定変更を試みることにしました。

バイアンプ接続 〇

このAVアンプにはバイアンプの設定があり、フロントスピーカーの高域、低域に1チャンネルずつ計4チャンネルを割り当ててパワーを増すことができます。まずは1段階目の驚きがありました。全体に音の圧力が増したように感じました。いまままでスピーカーの本当の力を出し切っていなかったのかと後悔しました。

ピュアダイレクトモード◎

AVアンプ内部のDSPを通さないで純粋なソースを再生するモード。これがまた驚きで、いままで聞こえなかった音が聞こえるようになったのはもちろん、いままでMP3などの劣化音源のせいかと思っていた音が正しく聞こえるではありませんか。AVアンプのDSPを通ることでいかに音が劣化していたかを思い知りました。一つの例は、ベースなどの低い音。E,F,Gあたりの音が、DSPを通ると、極端な話、音程がずれて聞こえるわけです。MP3だから?と思っていたのが間違いで、AVアンプのDSPを通ったことによる劣化だったわけです。

バイワイヤリング?

アンプの1chの出力を枝分かれさせて、スピーカーの高域、低域に二本ずつ計4本のケーブルでつなぐというつなぎ方も勉強しました。文献によると、長いスピーカーケーブルで高域における「位相」のずれが生じるところを比較的低く抑えることができるとのことでしたが、私の耳では実感できず。

この間学んだことは
  • いいアンプといいスピーカーといい音源だと、前後左右、目の前に演奏者がいるような感覚が味わえる。(音の解像度、定位感などのこと)
  • AVアンプのDSPはデジタルデータが様々に加工されて再合成される都合どうしても劣化が避けられない。(DSPの精度性能に依存)
  • 多チャンネルのAVアンプの音質はどう転んでも同価格帯の純粋なプリメインアンプにはかなわない。
となると上を目指すには、20万円以上の高級プリメインアンプを買わないといけないのかと覚悟しました。ところが、実は、デジタルアンプという分野があって、いわゆる「中華アンプ」というジャンルは、格安、省電力、超小型で高級アンプに迫る音質を誇る分野があるという情報を入手しました。

20万円のプリメインに進む前に準備運動がてら、AMAZONなどで評価の高かった1台を購入しました。

FX-AUDIO- FX1002J 1万円弱。

160W級のパワーと、音質の評判は上々でした。AVアンプのプリアウトからデジタルアンプをフロント用のアンプとして接続しました。AVアンプはここでは、DACとして動作することになります。

驚きました。が、その音質については、世の中のブログに数々先輩方がレビューしておられるのでここでは省きます。

FX1002Jはオペアンプの交換ができるようになっており、これも先輩たちに倣ってMUSES8920という1つ500円程度2つを交換したところ、これまた驚きの音質向上がありました。

これまでの経緯をまとめると

AVアンプ 2ch < AVアンプ-バイアンプ < AVアンプ-バイアンプ+ピュアダイレクト <<< 中華デジタルアンプ < 中華デジタルアンプ+オペアンプ交換

このままデジタルアンプを極めるとすると
  • さらに高価な4000円弱もするオペアンプ MUSES01 を試してみたい・・・
  • DACとなっているAVアンプの代わりに、これもネットで話題の中華製DACで再生してみたら・・・
などと欲が膨らみます。

Chromecast Audio のラインアウト(アナログ)は使えるか?

答え:使えない

Chromecast Audioではかの有名のIFIXITの分解によると旭化成のDAC AK4430というチップが使われています。192kHz 24bit対応の高性能です。

一方で、Chromecastは出力として、ステレオミニジャックで、アナログと光出力の2系統を用意しています。

想定できる接続形態は

1. Chromecast Audio → (内臓DACのアナログ出力)→ (メイン)アンプ → スピーカー

2. Chromecast Audio → (内臓光出力)→ (外部)DAC →(メイン)アンプ → スピーカー

の2通り考えられます。

私自身は、2の構成、AVアンプをDAC兼プリアンプとして使っていたので光出力での音質を堪能していたのですが、小さなChromecast Audioに内蔵されているAK4430なるDACのそのままの出力をアンプに直結してもそれなりにいい音が出るのではないかと思い試してみました。

結果は見事玉砕。ぺしゃんこの音になってしまいました。Chromecast Audioをいい音で聞きたければ是非とも光出力を利用しましょう。

Bluetooth aptX vs Chromecast Audio 音質対決

Bluetooth aptXでも音質劣化は避けられない

これまで Google Play Music のHiFiアンプへの再生方法として Bluetooth aptX で再生機器へ飛ばし、光デジタル出力をAVアンプの入力として再生していました。Google Play Musicが一般のDLNA機器へ直接飛ばせないのは、音楽コンテンツの著作権保護があるのだろうなと推察していますが、なにぶん、Bluetoothで飛ばしたところで、MP3 320kbps相当といわれるGoogle Play Musicの音質が損なわれていることで不満を持っていました。

Chromecast Audioなら高音質でGoogle Play Music再生

Chromecast Audio は Androidなどで対応したアプリから直接音声を飛ばせる機器で、通常のDLNAデバイスとしても機能します。24bit 96kHzのいわゆるHDクォリティをサポートしており、MP3 320Kbps 相当といわれる Google Play Music を最大限の音質で再生するための一番よいデバイスということなになります。

Google Play Music なら aptX でもほぼ変わらない音質だった

今回 Google Chromecast Audio を導入したので聞き比べをしてみました。

  • Google Play Music (MP3 320kbps相当)
  • FLAC 16bit 44khz(CD 無圧縮相当)
  • FLAC 24bit 96kHz(HDサウンド)

で比べてみましたが、私の(いいと思っている)耳で判別できる限りではGoogle Play Musicの音質はChromecastでもBluetooth aptXで聞いてもほぼ同じレベル。Chromecastはおそらく中でMP3をそのままデコードしているだけですから、一方で、いったんデコードされた音声を aptXで再圧縮して送ったにもかかわらず、それなりの音質を保っていることからいかに aptX が優秀かがわかります。

差が出たのは、FLAC 16bit 44.1kHz(いわゆるCDの無圧縮音質)から上の領域でした。私の(いいと思っている)耳でも明らかにその差がわかります。(24bit 96kHzの領域は音源が少ないのと、今回使ったスピーカーの限界でよくわかりません。)



Google Play Music
(MP3 320kHz)
FLAC 16bit 44.1kHz
FLAC 24bit 96kHz

Bluetoothレシーバー(aptX)




Chromecast Audio









今回使った機器は次のとおり

Chromecast Audio

https://www.google.co.jp/intl/ja_jp/chromecast/speakers/

Hanwha apt-X/AAC対応 Bluetoothレシーバー HS-BMR002

http://www.amazon.co.jp/dp/B00I04Z314/ref=cm_sw_r_tw_dp_KHruxb0F6SAB2

ONKYO GX-D90

http://www.jp.onkyo.com/pcaudio/poweredspeaker/gxd90/


Welcome to BlogEngine.NET

If you see this post it means that BlogEngine.NET is running and the hard part of creating your own blog is done. There is only a few things left to do.

Write Permissions

To be able to log in, write posts and customize blog, you need to enable write permissions on the App_Data and Custom folders. If your blog is hosted at a hosting provider, you can either log into your account’s admin page or call the support.

If you wish to use a database to store your blog data, we still encourage you to enable this write access for an images you may wish to store for your blog posts.  If you are interested in using Microsoft SQL Server, MySQL, SQL CE, or other databases, please see the BlogEngine docs to get started.

Security

When you`ve got write permissions set, you need to change the username and password. Find the sign-in link located either at the bottom or top of the page depending on your current theme and click it. Now enter "admin" in both the username and password fields and click the button. You will now see an admin menu appear. It has a link to the "Users" admin page. From there you can change password, create new users and set roles and permissions. Passwords are hashed by default so you better configure email in settings for password recovery to work or learn how to do it manually.

Configuration and Profile

Now that you have your blog secured, take a look through the settings and give your new blog a title.  BlogEngine.NET is set up to take full advantage of many semantic formats and technologies such as FOAF, SIOC and APML. It means that the content stored in your BlogEngine.NET installation will be fully portable and auto-discoverable.  Be sure to fill in your author profile to take better advantage of this.

Themes and Plugins

One last thing to consider is customizing the look and behavior of your blog. We have themes and plugins available right out of the box. You can install more right from admin panel under Custom.

On the web

You can find news about BlogEngine.NET on the official website. For tutorials, documentation, tips and tricks visit our docs site. The ongoing development of BlogEngine.NET can be followed at Github. You can also subscribe to our Youtube channel.

Good luck and happy writing.

The BlogEngine.NET team

Javascrip で作る FlashAir (TM) クライアントアプリ - FlashAir W-03 の挙動まとめ

東芝 FlashAir W-03 の挙動まとめ

「FlashAirをいじってわかったこと。」

https://sites.google.com/site/gpsnmeajp/electricmemo/flashair
にいろいろとまとめてくださってあり、非常に参考になります。


ここでは、ウェブプログラマ的視点で、私が気が付いたことを、ネタがかぶらない範囲でまとめておきます

ちなみに私の手持ちのFlashAir W-03のファームウェアバージョンは

FA9CAW3AW3.00.00

GET /command.cgi のキャッシュ防止には &TIME=xxxx を付ける。 → jQueryの {cache:false}は使えない。


HTTPのGETメソッドは通常のアクセスと同様に、ブラウザがキャッシュすることで同じページを2度目に開くときに時間を短縮します。が、時によってこの機能が邪魔な場合があります。
FlashAirの場合 command.cgi?op=121 でSDカードの更新状態を定期的にチェックするのですが、ブラウザがこの呼び出しの結果をキャッシュしてしまうとSDカードが更新されてもずっと同じ結果(タイムスタンプ)が返ってくるため期待した結果と異なることになります。
そこで、簡単な解決策としてアクセスするURLを少しずつ変えることでいつも新しいページを開くようにブラウザのキャッシュを回避する方法があります。
GET /command.cgi?t=10000
とアクセスした後
GET /command.cgi?t=10001
と適当に異なる番号をつければ、ブラウザは別のページだと解釈してかならず、サーバーから新しいコンテンツを取り寄せます。

ところが、FlashAirの /command.cgiは ?TIME=xxxx という書式は受け付けますが ?t=xxxx という書式だとなぜかエラー。
さらに困ったことに、、
jQuery.ajax には  { cache: false } というオプションがあって先に説明した方法でブラウザのキャッシュを回避するオプションがあるのですが、これが t=xxxx という書式でURLの末尾に追加する方法なのです。このオプションはFlashAirに対しては使えません。

SDカードにカンマ(,)を含むディレクトリ名があるとそのページの自動更新が失敗する。


ウェブブラウザで接続した際の単純なバグです。

https://flashair-developers.com/ja/documents/api/commandcgi/#100
ではカンマを含むファイル名、ディレクトリ名について言及しているだけに残念です。
List.htmに挿入するための <!--WLANSDJLST--> というキーワードは" "で囲まれた書式のため心配が要りませんが
http://flashair/command.cgi?op=100&DIR....
の結果はCSV表現なのですがカンマの含まれる名前はEXCELなどでなされる""で囲むなどの処理がされていないため、読み込みプログラム側で柔軟に解析しないと失敗します。拙作の FlashAir List.htmChromeアプリ版 FlashAir Sync はこれの対策を入れてあります。

同時にダウンロードできるファイルは1つだけ ← HTTP(WiFi)経由でファイルをダウンロード中は API の応答が停止する。


ApacheやIISなどモダンなウェブサーバーになれているとハッとさせられる挙動です。大きな画像ファイル、デジカメのRAWファイルなどある程度転送の時間がかかる場合に注意が必要です。
ファイルは1つずつしか転送できません。APIにアクセスするアプリが同時に多数ある場合は、応答のパフォーマンスに配慮する必要があります。
当初、私は、複数のディレクトリを指定してゲリラ的にローカルと同期させるようなアプリを考えていましたが、この制限によって、ファイルの自動転送は、ディレクトリごと1つ1つが正しい作法だということになりました。

※WiFi経由の転送は同時に1つに制限されますが、その間SDカードへの直接書き込みはできまるから、デジカメでの撮影が滞ることはありません。

ブラウザユーティリティー <!--WLANSDJLST--> より command.cgi?op=100 のほうが速い


SDカードに内蔵のウェブサーバーは、小電力の限界もあってパフォーマンス的に期待するのは酷なのは想像に難くありません。
結論としては、SDカード側で難しいことはさせず、複雑な処理はできる限りクライアント側でやらせるというアプローチがいいことになります。

当方では List.htm のカスタマイズを公開していますが、当初 <!--WLANSDJLST--> を使ったアプローチで作成しましたが、
ディレクトリを移動するごとに HTMLのすべての転送、js/cssなどのファイルの転送が発生し、ページの表示が完了するまで数秒の待ちが発生してしまいました。
FlashAirオリジナルの List.htm の場合も同様です。
そこで、改良版として List.htm に <!--WLANSDJLST--> を一切含まないものを作成して、ファイルのリストはAPIの
/command.cgi?op=100&DIR=....
で取得するように変更しました。
その結果ページの表示に関して2割程度のスピードアップ。さらに、ディレクトリの移動はURLをリンクするのではなく、
ページ内のJavascriptからAJAXでAPIからファイルリストを取得しなおす「シングルページアプリ」として作ることで、
ディレクトリの移動も含め劇的に快適なスピードでアクセスできるようになりました。

ファイルアップロードのHTTP通信で注意すべきこと

ファイルのアップロード upload.cgi は本家ドキュメントで書かれていないことが数点あります。

  • アップロードの multipart/form-dataの作り方で、Content-Disposition: ... filename="FILENAME.JPG"
    のように filename を必ずダブルクォーテーションでくくる必要があります。.NETの一部のAPIでこれを付けないものがあるので要注意。
  • ファイルアップロードで upload.cgi 呼び出しの戻りは text/html です。SUCCESS/ERROR ではありません。

FlashAirのウェブサーバーはブラウザのキャッシュ機構に優しくない・・・

ブラウザカスタマイズで List.htm を作成した場合、関連する jsやcssファイル、画像ファイルなどがSDカード側からロードされるのですが、どうやら、サーバー(FlashAir)のレスポンスに、サーバーがLast-Modifiedなどキャッシュ関連のヘッダーが付いておらず、唯一ETag がついているもののウェブブラウザからの If-None-Match に応答している気配がありません。したがって、ページを開くたびに関連するファイルがすべて転送されることとなり、ページが表示しきるまでの待ち時間がそれなりに生じてしまいます。

拙作 List.htm カスタマイズではディレクトリの移動を ajax でのみ更新するシングルページアプリとしたことでこの問題を回避しています。

luaスクリプトは・・・将来に期待


電子工作などの活用でいろんな記事を見ていると可能性があって面白いです。ただ、「写真を共有」「ファイルをダウンロード」的な典型的な使い方の範囲では先述の理由もあって、パフォーマンス、電力消費、安定性などからAPIを軽くたたきつつ、重い処理はウェブブラウザで・・・というアプローチが現状快適です。

たとえば、command.cgi?op=121 更新タイムスタンプの取得 はSDカードのどこかのディレクトリに変更があった場合に更新されますが必ずしも現在注目している(ブラウザで開いている)ディレクトリであるとは限りません。luaとの組み合わせで、特定のディレクトリのみをモニターする仕組みを考えたのですが、command.cgi?op=100 で変更があったかもないかもしれないファイルリストの列挙を呼び出すだけのほうが実はFlashAirの負荷が少ないんじゃないかと思ったりします。

一方で、1つのディレクトリに1万個以上の大量のファイルがある場合に、ファイルリスト取得のAPIは毎回巨大サイズのCSVファイルを転送することになり非効率です。欲を言えば、 command.cgi?op=100にページングのオプションが付けられないでしょうか。




Javascriptで作るFlashAir(TM)アプリ 4種

TOSHIBA FlashAir(TM) 職人心をくすぐる製品でしたので勢いあまって作ったユーティリティーをオープンソースコミュニティの役に立てればと放出します。

FlashAir Javascript client library

JavascriptからFlashAirのAPIをたたきます。ajaxの通信、日付、各種ステータス情報を、オブジェクト化およびカプセルしています。クライアントクラスは command.cgi?op=121 によるSDカードの更新状況の自動通知機能も装備されています。

ソースコードは

  • TypeScript(.ts)のソースコード
  • .tsをコンパイルした Javascript(.js) ファイル
  • Javascript(.ts) を uglify で圧縮した (.min.js)ファイルと .map ファイル
をパッケージしています。


以下のアプリはこのライブラリを使って作られています。

FlashAir List.htm カスタマイズ

FlashAirの隠しフォルダ SD_WLAN に List.htm というファイルを作ることでウェブブラウザ経由のページをカスタマイズできます。レスポンシブデザイン、およびサクサクパフォーマンスのカスタマイズを公開しています。ぜひお試しください。

利用はGitHubから [Download ZIP] でZIPファイルをダウンロードして SD_WLAN ディレクトリを上書きします。

FlashAirList (GitHub)





Yokin's FlashAir Sync (Chromeアプリ版)

同種の名前がいくつかあって、かぶりますが、その名の通り、クライアントとなるPCとFlashAirのファイル転送を手助けするツールです。Chromeアプリとして作られているので、Google Chromeの動作するWindows Linux Macなどで動作します。入手はGoogle WebStoreからどうぞ。ソースコードも公開(準備中)しています。


FlashAir Sync (Windows Store版)

同じJavascriptのライブラリを使用して作れるという点で、Windows 8専用のWindowsストアアプリが WinJSというJavascriptの開発プラットフォームを利用します。

https://www.microsoft.com/ja-jp/store/apps/yokins-flashair-sync/9nblggh1mtvm


FlashAir Sync (Cordova版) 希望

同じJavascriptの開発プラットフォームならAndroid iOS対応のアプリも開発ができるはずです。・・・目下遠い目標として。


jCryption.NET HTTP FORM POSTの暗号化

Fork me on GitHub

jCryptionとは

http://www.jcryption.org/

jCryptionはウェブページのフォームデータをHTTPでPOSTする際に、データをRSA/AESで暗号化してサーバーに送信するjQueryのプラグインライブラリです。

jCryption.NETはjCryptionの送信した暗号データをサーバーで復号化するためのASP.NET用のライブラリです。

背景

ログインフォーム、問い合わせフォーム、アンケートフォームなど、ウェブページ上で利用者の入力を受けサーバーへデータを送信する場面は多くあります。一方で、個人情報、IDやパスワードなど他者に見られたくない内容を含む場合、そのフォーム送信は「暗号化」によって保護された状態で送信されるべきで、通常、平文通信であるHTTPでの送信は避けHTTPSで保護されるべきです。しかし、HTTPS/SSLの通信をサポートするためには高額(ベリサインの場合8万円程度)のサーバー証明書のための費用とウェブサーバーのIPアドレスを一つ独占するというコストがかかり、ちょっとしたアンケートフォームなどの目的には導入を躊躇する場面もあります。

本格的にウェブサーバーとの通信にHTTPS/SSLを導入することなく、クライアントからサーバーへ送信される情報を暗号化により保護しようというのがjQueryのプラグインであるjCryptionというJavascriptのライブラリのコンセプトです。

一般にHTTPS/SSLには

  1. 通信の相手であるサーバーが本物であるかどうかを保証すること(サーバー証明書)
  2. 通信中のデータの暗号化

の2種類の目的がありますが、jCryptionは2つ目の通信中のデータの暗号化のみをサポートします。

jCryptionが使用する暗号は RSA/AESで、RSAはクライアント側で生成されたAESの鍵を暗号化してサーバーへ送信するために使われます。実際にPOSTされるフォームのデータはAESで暗号化されて送信されます。

これから先のjCryptionそのものの詳しい内容は本家のウェブサイトに譲ります。http://www.jcryption.org/(英語)

ここでは、jCryption.NETを使った例を紹介します。

jCryption.NET

ソースコードとサンプルはGitHub で公開しています。

クライアント側の使用法は本家で解説されているように、jQueryとjCryptionのライブラリを<script>で読み込み
 $(“#form”).jcryption(….)
で指定したフォームをjCryption対応にセットします。
このフォームを送信するにあたって、暗号の鍵の交換、データの解読にサーバー側のプログラムが必要になります。本家のパッケージではPHPやPerl CGIによる実装がすでに含まれています。ここでは、Windows Server + IIS + ASP.NETな環境でサーバー側の実装を紹介します。

想定環境

  • IIS6以上のウェブサーバー(Windows 2003 Server以降)
  • .NET 4.0 以上の.NET Framework
  • ASP.NET WebPagesと呼ばれる cshtml ファイルによるページ

ASPXページの場合

ページをjCryption.SecurePage を継承させることで、暗号処理を処理させられます。javascript部分は手作業で挿入します。
<%@  Language="C#" Inherits="jCryption.SecurePage" %>
<html>
<head>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
    <script type="text/javascript" src="Scripts/jquery.jcryption.3.1.0.js"></script>
    <script type="text/javascript">
        $(function () {
            $("#normal").jCryption({
                getKeysURL: "<%= Request.Path %>?getPublicKey=true",
		            handshakeURL: "<%= Request.Path %>?handshake=true"
		        });
		    });
    </script>
</head>

CSHTMLページの場合

  1. jCryption.csファイルを App_Codeフォルダへ配置します。
  2. フォームのあるcshtmlページの先頭部分に@using jCryption を記述します。
  3. 鍵の交換、復号化などを同じページ上(URL)で行うため、処理の初めに
    jCryption.HandleRequest(Request);
    を呼び出します。この呼び出しの後で、Request.Formは復号化されたデータにアクセスできます。
  4. HTML内で、依存するjqueryを読み込みます。jquery.validateも読み込み可能です。
  5. HTML内、上記のjqueryの読み込みの後、適当な場所に
    @jCryption.RenderScriptFor(“#login”, src:”/Scripts/jquery.jcryption….js”);
    を記述します。jcryptionのjsファイルを直接<script>で指定しないのは、このページがHTTPSでアクセスされた場合に、自動的にjcryptionを無視して読み込まない動作を行うためです。

@using jCryption
@{
    jCryption.HandleRequest(Request);
}
<!doctype html>
<html>
   <head>
      <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
      @jCryption.RenderScriptFor("#normal", src: "Scripts/jquery.jcryption.3.1.0.js")
   </head>
   ......
   <form id="normal">
      .....
   </form>
   ......

フォームの初期データ、部分的にHTMLを暗号化して読み込む機能

jCryption.NETをcshtmlで使用する場合に、フォームデータと、HTMLテキストの暗号化にも対応しています。もともとjCryptionはクライアントからサーバーへ送信されるデータの保護を目的としていますが、ログインIDやパスワード、住所氏名などはフォーム上であらかじめわかっている内容を入れてHTML出力されることがあります。この場合HTTPのままですと、データは平文で保護されておらず、片手落ちとなります。

  1. フォームの各要素を次のように記述します。
    <input type=text name=Name value=”山田” />は
    <input type=text @jCryption.SecureNameValue(“Name”,”山田”) />

    チェックボックスやラジオボタンは
    <input type=”checkbox” @jCryption.SecureNameValueCheck(“Sex”,”Male”, true ) />

  2. HTMLまたはテキスト領域を暗号化で保護したい場合は
    @jCryption.SecureHtml()
    @jCryption.SecureText()
  3. 上記の呼び出しの後に
    @jCryption.LoadSecureContents()
    を呼び出します。ここに、一連の復号化処理、サーバーとの通信処理が記述されます。

ライブラリリファレンス

@jCryption.RenderScriptFor( String selector, String src = null, String script = null )

jCryption を利用するFORMをjQueryのセレクタで指定します。この呼び出しは通常 head タグか body 内最後の方に配置します。jquery.validate と同時に使用する場合は jquery.validate を読み込む script タグの後になるようにします。

selector: jCryptionで初期化する対象のFORMを jQuery のセレクタで指定します。

src: jCryption のjsファイルへのパスを指定します。

script: その他のjavascriptが必要な場合に <script>...</script>を含むHTMLを指定します。


@jCryption.SecureNameValue( String name, String value )

フォーム要素の初期状態で表示されるデータを保護します。<input> タグ内に記述します。.

 <input type='text' @jCryption.SecureNameValue("Name", "Smith") />

input タグの name と value を引数に指定します。


@jCryption.SecureNameValueCheck( String name, String value, bool check )

input の checkbox radiobutton に対応したバージョンです。
    <input type='checkbox' @jCryption.SecureNameValueCheck("Animal", "Dog", true ) />
    <input type='checkbox' @jCryption.SecureNameValueCheck("Animal", "Cat", false ) />

@jCryption.SecureText( String text )

暗号化されたテキストコンテンツを設定します。HTML内の任意の位置に記述可能です。<div>で囲まれ暗号化データとともにHTMLとして出力されます。

@jCryption.SecureHtml( String html )

暗号化されたHTMLコンテンツを設定します。HTML内の任意の位置に記述可能です。<div>で囲まれ暗号化データとともにHTMLとして出力されます。

@jCryption.LoadSecureContents()

SecureNameValue/SecureNameValueCheck/SecureText/SecureHtml の内容を保護された実際のデータをDOM要素中に読み込むことを指示します。上記4種類メソッド呼び出しの一番最後にこの呼び出しを追加してください。

jCryption version 3.1.0に内在するいくつかの技術的問題点

最後に本家jCryptionに見られるいくつかの技術的問題点を指摘しておきます。

  1. AESの鍵が<form>単位で異なる問題
    クライアントはformのsubmit時にAESキーを生成しajax呼び出しでサーバーに通知します。この時サーバーはSessionにAES鍵を保存します。同じSession上で同時にformのsubmitが起こることはまれですが、原理上、クライアントとサーバーで鍵の1対1の整合性が保てていません。
  2. 先の理由により、<form> POST後にブラウザのF5(更新)によって再度POSTした場合にSession上に保存された鍵との一貫性の保証がなく、再POSTがエラーとなります。これには、POSTデータに暗号化されたフォームデータとともに暗号化されたAES鍵も同時に送信されるようにすべきです。
  3. jquery.validateライブラリと一緒に使用したときの問題があります。jCryptionはSubmitボタンclickイベントをフックして暗号化処理ののち改めてformのsubmitを呼び出します。一方で、jquery.validateはformのsubmitイベントでvalidationを実行しています。処理順序が逆転することで2つのライブラリを同時に使用することができません。(jCryption.NETではこの点を改善するjavascriptのコードを出力しています。)
※上記の問題を修正したバージョンを jquery.jcryption.3.1.0.mod.js として同梱しています。


ASP.NET WebPagesと相性のよい w2ui grid

ASP.NET WebPages と Razor(cshtml)の組み合わせに相性のよいTableソリューションを求め歩いていたところ DataTablase に続いて w2ui という優れたライブラリを発見。

w2ui はレイアウト、グリッド、ツールバー、サイドバー、タブ、フォーム、ポップアップその他のユーティリティで構成されるライブラリですが、軽量設計で使いやすくまとまっています。

今回Gridを試したわけですが、このGridの優れたところは、ページングの概念を捨てて「無限スクロール」を導入したこと。しかもその動作は軽快です。

DataTablesと同様にサーバーへは form data、サーバーからはJSONによる応答です。form dataの仕様がこちらのほうが簡単で使いやすいかもしれません。また、開発中の Ver 1.4 には拙翻訳を提供したので日本語表記の利用も可能です。

jQuery DataTablesでやったことと同じことを w2ui を使って再現してみます。

1:  @{  
2:       if (IsPost)  
3:       {  
4:            if (UrlData[0] == "GetList" )  
5:            {  
6:                 var all = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.AllCultures).AsEnumerable<System.Globalization.CultureInfo>();  
7:                 var filtered = all;  
8:                 int iDisplayStart = Request["offset"].AsInt(0);  
9:                 int iDisplayLength = Request["limit"].AsInt(10);  
10:                 bool sorted = false;  
11:                 for (int i = 0; Request[String.Format("sort[{0}][field]", i)] != null; i++)  
12:                 {  
13:                      String fld = Request[String.Format("sort[{0}][field]", i)];  
14:                      bool asc = Request[String.Format("sort[{0}][direction]", i)] == "asc";  
15:                      switch( fld)  
16:                      {  
17:                           case "Name":  
18:                                filtered = asc ? filtered.OrderBy(m => m.Name) : filtered.OrderByDescending(m => m.Name);  
19:                                break;  
20:                           case "DisplayName":  
21:                                filtered = asc ? filtered.OrderBy(m => m.DisplayName) : filtered.OrderByDescending(m => m.DisplayName);  
22:                                break;  
23:                           case "TwoLetterISOLanguageName":  
24:                                filtered = asc ? filtered.OrderBy(m => m.TwoLetterISOLanguageName) : filtered.OrderByDescending(m => m.TwoLetterISOLanguageName);  
25:                                break;  
26:                           case "ThreeLetterISOLanguageName":  
27:                                filtered = asc ? filtered.OrderBy(m => m.ThreeLetterISOLanguageName) : filtered.OrderByDescending(m => m.ThreeLetterISOLanguageName);  
28:                                break;  
29:                      }  
30:                      sorted = true;  
31:                 }  
32:                 if (!sorted) { filtered = filtered.OrderBy(m => m.Name); }  
33:                 for (int i = 0; Request[String.Format("search[{0}][field]", i)] != null; i++)  
34:                 {  
35:                      String fld = Request[String.Format("search[{0}][field]", i)];  
36:                      String sSearch = Request[String.Format("search[{0}][value]", i)];  
37:                      if (!String.IsNullOrEmpty(sSearch))  
38:                      {  
39:                           switch (fld)  
40:                           {  
41:                                case "Name":  
42:                                     filtered = filtered.Where(m => m.Name.Contains(sSearch));  
43:                                     break;  
44:                                case "DisplayName":  
45:                                     filtered = filtered.Where(m => m.DisplayName.Contains(sSearch));  
46:                                     break;  
47:                                case "TwoLetterISOLanguageName":  
48:                                     filtered = filtered.Where(m => m.TwoLetterISOLanguageName.Contains(sSearch));  
49:                                     break;  
50:                                case "ThreeLetterISOLanguageName":  
51:                                     filtered = filtered.Where(m => m.ThreeLetterISOLanguageName.Contains(sSearch));  
52:                                     break;  
53:                           }  
54:                      }  
55:                 }  
56:                 var output = new  
57:                 {  
58:                      status = "success",  
59:                      total = filtered.Count(),  
60:                      records = filtered.Skip(iDisplayStart).Take(iDisplayLength).AsEnumerable().Select((o, i) => new { recid = o.Name, o.Name, o.DisplayName, o.TwoLetterISOLanguageName, o.ThreeLetterISOLanguageName })  
61:                 };  
62:                 Response.ContentType = "application/json";  
63:                 Response.Write(Json.Encode(output));  
64:                 Response.End();  
65:                 return;  
66:            }  
67:       }  
68:  }  
69:  <script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-1.9.0.js"></script>  
70:  <link rel="stylesheet" href="http://w2ui.com/src/w2ui-1.3.min.css" type="text/css" />  
71:  <script src="http://w2ui.com/src/w2ui-1.3.min.js"></script>  
72:  <script>  
73:    $(document).ready(function () {  
74:         $('#sampleTable').w2grid({  
75:              name: 'grid',  
76:              header: 'List of Names',  
77:              show: {  
78:                   toolbar: true,  
79:                   footer: true  
80:              },  
81:              columns: [  
82:                      { field: 'Name', caption: 'Name', size: '20%', sortable: true, attr: 'align=center' },  
83:                      { field: 'DisplayName', caption: 'DisplayName', size: '30%', sortable: true, resizable: true },  
84:                      { field: 'TwoLetterISOLanguageName', caption: 'TwoLetterISOLanguageName', size: '30%', sortable: true, resizable: true },  
85:                      { field: 'ThreeLetterISOLanguageName', caption: 'ThreeLetterISOLanguageName', size: '30%', resizable: true }  
86:              ],  
87:              searches: [  
88:                      { field: 'Name', caption: 'Name', type: 'text' },  
89:                      { field: 'DisplayName', caption: 'DisplayName', type: 'text' },  
90:                      { field: 'TwoLetterISOLanguageName', caption: 'TwoLetterISOLanguageName', type: 'text' },  
91:              ],  
92:              sortData: [{ field: 'Name', direction: 'ASC' }],  
93:                 url: '@Href(Request.AppRelativeCurrentExecutionFilePath,"GetList")'  
94:         });  
95:    });  
96:  </script>  
97:  <div>  
98:       <div id="sampleTable" style="height: 600px;">  
99:       </div>  
100:  </div>