2015年9月2日水曜日

BCDを計算するメソッド

たまーに使われてる二進化十進表現(BCD)をC#で扱うためのメソッド。

二進化十進表現とは10進数の何桁かの数字を16進数で見やすくするためのエンコードの事。二進化というけど、2進数での表現ではあまり人間にはメリットがない。
そもそもBCDが便利ということはほとんど無く、実際に使うのは7セグLEDデコーダチップのインターフェースだったりとかで使われているだけ。他にはシリアルポートに特定のデータを流したくない場合にBCDでエンコードするとかもやるみたい。例えばスタートバイトをとストップバイトを0xF0以降に配置しておき、数値データはBCDで流した場合、16進表示のシリアルモニタで見るときに人間がわかりやすいし、BCDエンコードしてしまえば0x00-0x99の範囲しか扱わないのでストップバイトと競合することもない ということらしい。

本体
public class BCD
{
    static public UInt64 To(UInt64 HEX)
    {
        UInt64 val = 0;

        val |= (HEX % 10) << 4 * 0; HEX /= 10;
        val |= (HEX % 10) << 4 * 1; HEX /= 10;
        val |= (HEX % 10) << 4 * 2; HEX /= 10;
        val |= (HEX % 10) << 4 * 3; HEX /= 10;
        val |= (HEX % 10) << 4 * 4; HEX /= 10;
        val |= (HEX % 10) << 4 * 5; HEX /= 10;
        val |= (HEX % 10) << 4 * 6; HEX /= 10;
        val |= (HEX % 10) << 4 * 7; HEX /= 10;
        val |= (HEX % 10) << 4 * 8; HEX /= 10;
        val |= (HEX % 10) << 4 * 9; HEX /= 10;
        val |= (HEX % 10) << 4 * 10; HEX /= 10;
        val |= (HEX % 10) << 4 * 11; HEX /= 10;
        val |= (HEX % 10) << 4 * 12; HEX /= 10;
        val |= (HEX % 10) << 4 * 13; HEX /= 10;
        val |= (HEX % 10) << 4 * 14; HEX /= 10;
        val |= (HEX % 10) << 4 * 15;

        return (val);
    }

    static public UInt64 From(UInt64 BCD)
    {
        UInt64 val = 0;

        val += (BCD & 0xF) * 1; BCD >>= 4;
        val += (BCD & 0xF) * 10; BCD >>= 4;
        val += (BCD & 0xF) * 100; BCD >>= 4;
        val += (BCD & 0xF) * 1000; BCD >>= 4;
        val += (BCD & 0xF) * 10000; BCD >>= 4;
        val += (BCD & 0xF) * 100000; BCD >>= 4;
        val += (BCD & 0xF) * 1000000; BCD >>= 4;
        val += (BCD & 0xF) * 10000000; BCD >>= 4;
        val += (BCD & 0xF) * 100000000; BCD >>= 4;
        val += (BCD & 0xF) * 1000000000; BCD >>= 4;
        val += (BCD & 0xF) * 10000000000; BCD >>= 4;
        val += (BCD & 0xF) * 100000000000; BCD >>= 4;
        val += (BCD & 0xF) * 1000000000000; BCD >>= 4;
        val += (BCD & 0xF) * 10000000000000; BCD >>= 4;
        val += (BCD & 0xF) * 100000000000000; BCD >>= 4;
        val += (BCD & 0xF) * 1000000000000000;

        return (val);
    }
}

単体テスト
[TestClass()]
public class BCDTests
{
    [TestMethod()]
    public void ToTest()
    {
        Assert.AreEqual(0x000000000000ul, BCD.To(0));
        Assert.AreEqual(0x000000000001ul, BCD.To(1));
        Assert.AreEqual(0x000000000009ul, BCD.To(9));
        Assert.AreEqual(0x000000000010ul, BCD.To(10));
        Assert.AreEqual(0x123456789012ul, BCD.To(123456789012));
        Assert.AreEqual(0x999999999999ul, BCD.To(999999999999));
    }

    [TestMethod()]
    public void FromTest()
    {
        Assert.AreEqual(0ul, BCD.From(0x0000000000000000));
        Assert.AreEqual(1ul, BCD.From(0x0000000000000001));
        Assert.AreEqual(9ul, BCD.From(0x0000000000000009));
        Assert.AreEqual(10ul, BCD.From(0x0000000000000010));
        Assert.AreEqual(123456789012ul, BCD.From(0x123456789012));
        Assert.AreEqual(999999999999ul, BCD.From(0x999999999999));
    }
}

Visual Studioの単体テストの本来の使い方をやっと(偶然)発見したのだけど、結構便利。あとどれくらい時間がかかったのかがわかるのでちょっとプレッシャーがかかる。Fromのテストでは1msec未満だが、Toのテストでは4msec以上かかっている。パフォーマンスの差が激しいのが不思議。無理やり解釈すれば、Toはコンパイラ最適化によってビット演算のみで計算され、Fromはビット演算以外に加算と乗算が発生しているため とか?

2015年8月26日水曜日

C#で熱画像っぽい色合いを作る その2

前回、IFとかで熱画像っぽい色を無理やり作って満足していたのだが、どうにも物足りなくなってきた。というのは、本家FLIRの熱画像が素晴らしかったからだ。

この画像は本家FLIRの熱画像のスケールをスペクトル解析したもの。赤、緑、青と、輝度が表示されている。白い輝度の線を見ればわかるが、綺麗に直線になっているのだ。つまり「輝度でモノクロ化」した場合に、熱画像が綺麗に白黒になる。これはカラーで作成した資料を白黒でコピーした場合に大きく力を発揮するだろう。


そして僕が作ったのがこちら。
前回よりは若干直線性が増していると思う。しかしまだ完全ではない。

次にこの色をつくるための計算式だ。

Rf = 0.05 + Math.Sin(Value * 1.8 - 0.05) * 0.9;
Gf = 0.5 + Math.Sin((Value + 1.4) * 3.2) * 0.4;
Bf = 0.5 + Math.Sin(Value * 1.6 - 1) * 0.7 + Math.Sin(Value * 6 + 0.4) * 0.5;

たったこれだけである。前回のIF処理のネストは何だったのか。でも三角関数を使えば三角波なども作れるようなので、三角関数の組み合わせで遊ぶのは結構楽しい。

上記の計算だが、赤と緑は1)オフセット、2)位相、3)周期、4)振幅の4個のパラメータで構成されている。青は上記に位相、周期、振幅がもう1セット加わった7個のパラメーターとなる。人間の手で調整するのは大変だが、目的の形をプログラムで判定することができれば総当りで最適な計算式を作れるかもしれない。特に直線性は総当りで調べて行ったほうが簡単に作れる気がする。ただ、直線性だけを見たのでは熱画像風に使えるとは限らないので、候補を幾つか作らせて人間が確認する、ということになるだろうが。

とにかく、今回わかったことは、IFなんて使わなくていい ということだ。パラメータを調整するだけで任意のスペクトルを作れるんだから、次はもうちょっと最適化した計算を報告できるようにしたい。

C#で熱画像っぽい色合いを作る



昔の熱画像風ではなく、FLIRの熱画像風の色合いになってる。また上半分は結構のっぺりした感じになってる。これは赤と緑が早々と飽和してしまうため。
ウインドウ内、上は0-1の熱画像風、下は各色の割合を表示している。また下のウインドウは上のそれをPaint.netで白黒化したものとなる。それとデコボコしながら右肩上がりになっているグラフはRGBを輝度に近似したグラフとなっている。デコボコして入るが、一応右肩上がりになっているので白黒化してもBk-Whで変化しているのがわかると思う。
この熱画像風化では内部でサインテーブルを使用している(Math.Sinを使用)。そのために計算コストはかなり高いものとなっている。また画像を見てもわかるとおりに青は綺麗に増加させているわけではないため、多数の浮動小数点演算が行われる。今回はPCでちょっと使いたかったのでこのようになっているが、マイコンで使うにはかなり大変だろう。

2015年8月22日土曜日

C#でWaveファイルを作る

C#でWAVEファイルを作りたくなったのでメモ。

ヘッダ部分はCの構造体みたいな感じにスマートに作れるっぽいけど、とりあえず今回は1個1個ちまちま書き込んでる。
Visual Studio 2015のC#6からプロパティの初期化を行えるようになってちょっと便利になった(コンストラクタ作れって話だが)。

このプログラムはFormアプリケーションとして動作するが、起動時にデスクトップに"test.wav"というファイルを作ったらすぐに終了するようになっている。コンストラクタ内でCloseすると例外が発生するためやってはいけない。こういう「フォームなんて要らねぇよ」という場合はLoadイベントに匿名メソッドを追加し、その中でCloseするとヨサゲ。

作成するWAVEファイルは、44.1kHz16bit2chの標準的な感じの物。長さは10秒で、ch1/ch2共に1336Hzと941Hzを加算した正弦波を作る。今のところ、自動生成に対応しているのはヘッダのみ。実際の音声データは自前でバイナリレベルを扱う必要がある。でもまぁクラスに配列を渡すと最大4GiB近くを確保しようとしてOutOfMemoryExceptionが発生しちゃうから仕方ないね!!
BinaryWriterにちまちま書き込んでいけば4GB(4*10^9byte)の波形データでも扱える(書きだすのに非常に時間が掛かるが)。大容量のデータをR/Wする場合はすべてをバッファに読み出すことは不可能なので、その辺りをうまくゴニョゴニョする必要がある。

このプログラムで作ったWAVEをSoundEngine Freeで開いてFFTするとこんな感じ。


2015年8月16日日曜日

SQLiteに追加する

すでにSQLiteConnection SQLiteConnにデータベースが開かれているとする。

{
    string[][] Adds =
    {
        new string[] { "422100000", "422175000", "業務用ch5", "NFM", "Unidentified", "", "Unidentified", "0", "0", "Blackman-Harris 4", "8000", "440", "1", "82", "600", "", "2015-08-15T00:00:00", "0", "0" },
    };
            
    using (SQLiteTransaction sqlt = SQLiteConn.BeginTransaction())
    {
        using (SQLiteCommand command = SQLiteConn.CreateCommand())
        {
            foreach (string []Add in Adds)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append("insert into Data('Frequency','Center','Description','Mode','Protocol','Call Sign','Service','Shift Enabled','Shift','Filter Type','Filter Bandwidth','Filter Order','Squelch Enabled','Squelch','CW Shift','Notes','Date','Locked','Flagged') values(");

                foreach (string s in Add)
                {
                    sb.Append("'");
                    sb.Append(s);
                    sb.Append("',");
                }

                sb.Remove(sb.Length - 1, 1);
                sb.Append(")");

                //MessageBox.Show(sb.ToString());
                //Clipboard.SetText(sb.ToString());

                command.CommandText = sb.ToString();
                command.ExecuteNonQuery();
            }
        }
        sqlt.Commit();
    }

    MessageBox.Show("ok");
}

レコードを追加するにはSQLiteCommandのCommandTextプロパテのsetを使うらしい。"insert into Data"でDataテーブルに追加できる。要素名はテーブル名の後ろのカッコ内にカンマ区切りで書く。要素名は必ずしもシングルクォーテーションで囲む必要はないが、半角スペースとかが入る場合はシングルクォーテーションで囲む必要がある。とりあえずは囲んでおいたほうが無難。
データについては"values"のあとに書き込む。

今までデータを扱うようなプログラムを書いたことがなかったので、かなり苦労してる。まぁこれをちゃんと動くようにできれば別のものを作るときにはもっと楽にできる と考えて作ってるわけだが。最初っから趣味でプログラムを書いてると、大規模なプログラムになった時に大変だねぇ。。。

2015年8月15日土曜日

SQLiteを読む

SDR#のFrequency Managerは周波数をXMLで保存しているので、これを書き換えるにはExcelで十分なのだが、スキャンをしようとするとFrequency Manager + Scannerが必要で、こいつはXMLではなくData Base Fileで周波数などを保存している。そしてこれはSQLiteというモノらしい。
Frequency Manager + ScannerのUIは非常に使いづらく、周波数を5個入力するだけでも大変。ということでどうにかできないかと模索中。とりあえずSQLiteは読めるようになった。大抵の場合読めれば書くのは楽だが、Data Baseはどうなんだろう?

C#でSQLiteを読むにはここからsqlite-netFx45-setup-bundle-x64-2012-1.0.97.0.exeをダウンロードしてくる。今回はx64を使ったが、x32のほうが使い回しできていいと思う。またC#の参照に追加するにはインストールディレクトリから探してこないとダメみたい。僕の環境ではC:\Program Files\System.Data.SQLite\2012\binにSystem.Data.SQLite.dllが入っているので、こいつを追加した。またVisual StudioはExpress 2015を使ったが、Any CPUでは警告が出るために構成マネージャでx64のプラットフォームを設定した。これは2013とかでも同じ。

以下、SQLiteのデータベースを読むためのコード。

using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Text;
using System.Windows.Forms;

namespace FreqMgr
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void OpenButton_Click(object sender, EventArgs e)
        {
            string Path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\SDRSFM";
            string[] FileNames = FileSelect(Path, "FreqMgr.db", "Data Base File|*.db|All File|*.*");

            if (FileNames == null)
            {
                return;
            }

            List<string> tList = new List<string>();

            string[] DataNames = {
                "Id", "Frequency", "Center", "Description", "Mode", "Protocol", "Call Sign", "Service",
                "Shift Enabled", "Shift", "Filter Type", "Filter Bandwidth", "Filter Order",
                "Squelch Enabled", "Squelch", "CW Shift", "Notes", "Date", "Locked", "Flagged"
            };

            StringBuilder sb = new StringBuilder();

            using (SQLiteConnection conn = new SQLiteConnection("Data Source=" + FileNames[0]))
            {
                conn.Open();

                using (SQLiteCommand command = conn.CreateCommand())
                {
                    command.CommandText = "SELECT * FROM Data";

                    using (SQLiteDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            sb.Clear();
                            foreach (string DataName in DataNames)
                            {
                                sb.Append(reader[DataName].ToString() + ",");
                            }
                            tList.Add(sb.ToString().Substring(0, sb.Length - 1));
                        }
                    }
                }

                conn.Close();
            }

            sb.Clear();

            foreach (string DataName in DataNames)
            {
                sb.Append(DataName + ",");
            }
            sb.Remove(sb.Length - 1, 1);

            for (int i = 0; i < tList.Count; i++)
            {
                sb.Append("\n" + tList[i]);
            }

            //MessageBox.Show(sb.ToString());
            Clipboard.SetText(sb.ToString().Replace(',', '\t'));
        }

        private string[] FileSelect(string InitialDirectory, string FileName, string Filter, bool Multiselect = false)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.InitialDirectory = InitialDirectory;
            ofd.FileName = FileName;
            ofd.Filter = Filter;
            ofd.Multiselect = Multiselect;

            if (ofd.ShowDialog() != DialogResult.OK)
            {
                return (null);
            }

            return (ofd.FileNames);
        }
    }
}

データを表示するとこうなる。

とりあえずFrequency,Center,Description,Mode,Filter Bandwidthがあれば事足りそう。Dateは謎なデータだ。日しか使われていないし、UTCではなくローカルタイムのようだ。Tと書いてあるからといってUTC-07でもないらしい。おそらくDateは追加日で表示をソートするのに使ってる程度だと思うので、あんまり気にしなくても良さそう。

あとはSQLiteにデータを書き込むことができれば何とかなりそうだ。

2015年7月24日金曜日

PictureBoxで右クリック等を読み取る

C#のPictureBoxでクリック状態を検出する場合、Clickイベントでは右クリックを検出することは出来ません。右クリックを読み込むにはMouseDownイベントを使用する必要があります。

using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace PictureBoxClick {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();

            pictureBox1.MouseDown += pictureBox1_MouseDown;
            pictureBox1_MouseDown(pictureBox1, new MouseEventArgs(MouseButtons.None, 0, 0, 0, 0));
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e) {
            bool LeftClick = MouseButtons.None != (e.Button & MouseButtons.Left);
            bool RightClick = MouseButtons.None != (e.Button & MouseButtons.Right);
            bool MiddleClick = MouseButtons.None != (e.Button & MouseButtons.Middle);
            bool XButton1Click = MouseButtons.None != (e.Button & MouseButtons.XButton1);
            bool XButton2Click = MouseButtons.None != (e.Button & MouseButtons.XButton2);
            Point Location = e.Location;

            Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            using (Graphics gra = Graphics.FromImage(bmp))
            using (Font font = new Font("MS ゴシック", 12)) {
                gra.Clear(Color.Black);

                StringBuilder sb = new StringBuilder();

                sb.Append("Left    :" + LeftClick + "\n");
                sb.Append("Right   :" + RightClick + "\n");
                sb.Append("Middle  :" + MiddleClick + "\n");
                sb.Append("XButton1:" + XButton1Click + "\n");
                sb.Append("XButton2:" + XButton2Click + "\n");
                sb.Append("Location:" + Location + "\n");

                gra.DrawString(sb.ToString(), font, Brushes.White, new PointF());
            }

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
            pictureBox1.Image = bmp;
        }
    }
}

Leftは左、Rightは右、Middleはホイール、XButton1は戻るボタン、XButton2は進むボタン が割り当てられています。またこれらはMouseButtons列挙体のORされた値ですし、実際にnew MouseEventArgsで各ボタンをORして渡せば複数のボタンクリック状態を引数に与えることができます。しかし、実際の動作においては複数ボタンをクリックした場合、クリックした順番で複数回のイベントが発生します。これはクリックしたタイミングの問題というより、内部処理の問題のようです。例えば、user32.dllを使用して左クリックと右クリックをORして同時に行わせた場合、2回のイベントが発生します。基本的にMouseDownイベントだけではマウスの同時押しによるファンクションは行えないと考えたほうがいいでしょう。

どうしても同時押しを検出したい場合は以下のようにします。

using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace PictureBoxClick {
    public partial class Form1 : Form {
        bool LeftClick, RightClick, MiddleClick, XButton1Click, XButton2Click;

        public Form1() {
            InitializeComponent();

            pictureBox1.MouseDown += pictureBox1_MouseDown;
            pictureBox1.MouseUp += pictureBox1_MouseUp;
            pictureBox1_MouseDown(pictureBox1, new MouseEventArgs(MouseButtons.None, 0, 0, 0, 0));
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e) {
            System.Diagnostics.Debug.WriteLine(e.Button.ToString());

            if (MouseButtons.None != (e.Button & MouseButtons.Left)) {
                LeftClick = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Right)) {
                RightClick = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Middle)) {
                MiddleClick = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton1)) {
                XButton1Click = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton2)) {
                XButton2Click = true;
            }

            Draw();
        }

        private void pictureBox1_MouseUp(object sender, MouseEventArgs e) {
            System.Diagnostics.Debug.WriteLine(e.Button.ToString());

            if (MouseButtons.None != (e.Button & MouseButtons.Left)) {
                LeftClick = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Right)) {
                RightClick = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Middle)) {
                MiddleClick = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton1)) {
                XButton1Click = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton2)) {
                XButton2Click = false;
            }

            Draw();
        }

        void Draw() {

            Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            using (Graphics gra = Graphics.FromImage(bmp))
            using (Font font = new Font("MS ゴシック", 12)) {
                gra.Clear(Color.Black);

                StringBuilder sb = new StringBuilder();

                sb.Append("Left    :" + LeftClick + "\n");
                sb.Append("Right   :" + RightClick + "\n");
                sb.Append("Middle  :" + MiddleClick + "\n");
                sb.Append("XButton1:" + XButton1Click + "\n");
                sb.Append("XButton2:" + XButton2Click + "\n");

                gra.DrawString(sb.ToString(), font, Brushes.White, new PointF());
            }

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
            pictureBox1.Image = bmp;
        }
    }
}

このように、DownでフラグをTrueにし、UpでFalseにすれば、そのフラグを確認することにより任意のタイミングでマウス状態を獲得できます。ただ、この方法でも検出出来ない場合があることに注意が必要です。僕の環境では左・中・右の同時押しは検出できましたが、戻る・進む・右の同時押しは検出できませんでした。
そもそもマウスボタンの同時押しなど通常の動作状態ではありませんから、余程の理由がない限りは使用するべきではないと思います。