レベルメーター・コントロールを作る(1)


年の瀬ですね。年齢を重ねると1年が本当に短くて、こないだ餅を食ったなぁと思っていたら、また餅を食う季節です。

この半年ほどは早起きして散歩しているのですけれど、最近はさぶくてサボりがち。おかげで、プログラムを動かしたり、blogを書いたりする時間はたっぷりあるというわけです。なんと、この半年ほどで5Kg痩せました。特に食事制限などしていないのですが、僅かなことですけれど朝から動くのは身体に良いのでしょうね。いや、もしかしたら何か病気なのかもしれませんけどね、年寄りですし。

さて、人生へのグチも言い終わりましたので、本題。

 

本題

昨日、友人と電話で話をしたら、

「おまえの blog ってさ、Threadの話をしたかと思ったら、ヘッドルームの話とかさ、なんだか統一感なくておまえらしいよ」

と悪口を言われました。とっさに何も言い返せませんでしたので、すこし恨みを晴らしますと、要するにオーディオプレイヤーみたいなものを作りたいわけです。それも、ライブ配信で使いやすいようなやつ。その肩慣らしとして、音を再生したり、レベルメーターの話をしたり、Thread の性能を測ったりしているのです。

つまりここは練習場なわけ。イチローみたいにいろいろ計算しながら打撃フォームの実験を重ねているつもりなのですよ。それをさらに実況中継している。blog にまとめるっていう目標があれば、ちゃんとコードを書こうとするだろうし、読んでくださる方にもすこしは役に立つ。もしかしたらどこかをクリックしてくださるとても親切な方がいて、小銭がチャリーンと音をたてたりする。まあ、そんなことです。最後のあたりがちょっと世知辛い感じもしますけれどね。

そうそう、その友人には、

「アマゾンばっかり使っていると、日本はアメリカにやられてしまうから、国産のサービス使えよ!」

とかねがね言っております。この blog にもアマゾンをクリックできる枠が出ているのですが、これは私なりの抵抗といいますか、すこしでも日本国民たる私がアマゾンから搾り取るといいますか、そんな意味です。(言い訳です)

あれ、本題ちゃいますやん。

レベルメーターのプロトタイプ

ちゃんとやれという声が聞こえました。すみません。レベルメーターを作りたいんです。ええ、そうです。レベルメーターです。だいたいこんな仕様を考えます。できれば、C# .NET だけで何とかしたいのですが、難しいかもしれませんね。

  1. 0.1 秒ごとに書きかえる高速なメーター
  2. もちろんデシベル(dB)表示
  3. ピークホールド機能付き
  4. 最終的に NAudio から呼び出して使う

で、さっそくコードを書きますよ。今回は、カスタムコントロールを作ってみます。レベルメータなので、ざっくりとドット数(int DotNumber)、ドット同士の間隔(int DotMargin)を指定すると、自動的にコントロールの幅に合わせてラフに書くことにします。カスタムコントロール自体は、こんな感じになります。

public partial class LevelMeterBar : Control
{
	protected Stopwatch sw = new Stopwatch();
	public LevelMeterBar()
	{
		InitializeComponent();
	}
	///
/// レベルメーターのドット数
	///
	protected int dotNumber;
	[DefaultValue(10)]
	public int DotNumber
	{
		set
		{
			if (value > 0)
			{
				dotNumber = value;
			}
			else
			{
				dotNumber = 10;
			}
		}
		get
		{
			return dotNumber;
		}
	}
	///
/// レベルメーターのドット間隔
	///
	protected int dotMargin;
	[DefaultValue(1)]
	public int DotMargin
	{
		set
		{
			if (value > 0)
			{
				dotMargin = value;
			}
			else
			{
				dotMargin = 1;
			}
		}
		get
		{
			return dotMargin;
		}
	}

	(続く……)

肝心のところがありませんね。OnPaint でレベルメーターを書きます。FillRectangleで座標をずらしながらdotNumber 個描画する最も基本的なスタイルにしてみました。れいによって Stopwatch を使って描画にかかる時間を計測して、デバッグ出力しています。

protected override void OnPaint(PaintEventArgs pe)
{
	base.OnPaint(pe);
	sw.Reset();
	sw.Start();
	Rectangle rect = pe.ClipRectangle;
	Graphics g = pe.Graphics;
	Pen pen = Pens.Black;
	// 枠線を3重に書く(特に意味はない)
	rect.Inflate(-2, -2);
	g.DrawRectangle(pen, rect);
	rect.Inflate(-2, -2);
	g.DrawRectangle(pen, rect);
	rect.Inflate(-2, -2);
	g.DrawRectangle(pen, rect);
	// 念のためエラー処理
	if (dotNumber < 1)
	{
		dotNumber = 10;
	}
	// ドット幅を計算
	int dotwidth = (rect.Width - ((dotMargin) * dotNumber)) / dotNumber;
	if (dotwidth < 2) return; // いい加減だけど止める
	// 開始点 x,y
	int x=rect.X+1;
	int y = rect.Y + 1;
	// ドット高さ
	int dotheight = rect.Height-2;
	for (int i = 0; i < dotNumber; i++)
	{
		// ひたすら描く
		g.FillRectangle(Brushes.Green, x, y, dotwidth, dotheight);
		x += dotMargin + dotwidth;
	}
	sw.Stop();
	Debug.WriteLine("FillRectangle," + sw.ElapsedTicks + "," + sw.ElapsedMilliseconds);
}

別バージョンの描画方法

FillRectangle をループするのは能がないので、FillRectangles を使ってみましょう。LevelMeterBar を継承した、LevelMeterBar2 を作ります。OnPaint を override して置き換えます。肝心な部分だけ。

protected override void OnPaint(PaintEventArgs pe)
{
	//base.OnPaint(pe);
	sw.Reset();
	sw.Start();
	Rectangle[] dots = new Rectangle[dotNumber];

		//(中略)

	int x = rect.X + 1;
	int y = rect.Y + 1;
	int dotheight = rect.Height - 2;
	for (int i = 0; i < dotNumber; i++)
	{
		dots[i] = new Rectangle(x, y, dotwidth, dotheight);
		x += dotMargin + dotwidth;
	}
	g.FillRectangles(Brushes.Green, dots);
	sw.Stop();
	Debug.WriteLine("FillRectangles," + sw.ElapsedTicks + "," + sw.ElapsedMilliseconds);
}

今度は、ループの中はグラフィクスをいじらないで、Rectangle[] dots 配列に溜めておいて、最後に FillRectangles でドバッと書きます。

もう一つくらいやってみましょう。次のは、レベルメーターのドットを画像で用意しておいて、それを書きこむパターンです。DrawImageUnscaled を使います。Image DotImage はあらかじめ緑色の四角い画像を読みこんであります。

protected override void OnPaint(PaintEventArgs pe)
{
	//base.OnPaint(pe);
	sw.Reset();
	sw.Start();

		//(中略)

	for (int i = 0; i < dotNumber; i++)
	{
		g.DrawImageUnscaled(DotImage, x, y);
		x += dotMargin + dotwidth;
	}
	sw.Stop();
	Debug.WriteLine("DrawImageUnscaled," + sw.ElapsedTicks + "," + sw.ElapsedMilliseconds);
}

3つできました。まとめておきましょう。

  1. LevelMeterBar1:ぶんぶん回して FillRectangle で描画
  2. LevelMeterBar2:Rect に溜めこんで、最後に FillRectangles でドバッと放出
  3. LevelMeterBar3:ぶんぶん回して DrawImageUnscaled で描画

時間を計測する

もうすっかりお馴染だと思いますが、時間を測ったのでそれをグラフにしましょう。Form に3つのコントロールを張り付けて、同じ条件にするために、Size を 540, 40 で同じにして、DotMargin は 2、DotNumber は 50 で揃えます。DrawImageUnscaled の DotImage も指定しておきました。準備できました。計測です。

levelmeter1

だいたい、2814ticks/msec と分かっているので、それを使って ticks から msec に変換して Excel で平均処理時間を計算しました。

平均時間 ミリ秒 標準偏差
FillRectangle 1.80 0.24
FillRectangles 1.15 0.33
DrawImageUnscaled 5.69 1.51

DrawRectangle

やはり、FillRectangles は速いですな。Graphics に対する処理はまとめたほうがいいということのようです。しかし、それでも1ミリ秒ほどかかっています。0.1秒に1回程度書き換えるにしても、1ミリ秒は無視できません。レベルメーターだから最低でも2本は必要だし、しかもマルチチャンネル化したら10本ぐらいは欲しい。それに、実際のコードはさらに複雑になるだろうからその分を考えるとちょっと厳しいと思うのです。UIスレッドの処理は、どんなに長くても0.1秒で終わらないともたった感じになります。0.1秒=100ミリ秒間隔で処理するわけだから10ミリ秒ってのはUIスレッドの10%も使ってしまうわけです。

そこで、Thread を使ってうまく処理できないかって思っているわけです。最初のグチに戻るけど。DirectX とか XNA みたいなところに入っていくと、それはそれでいい選択しなのかもしれません。でも、わたくしごときの片手間徘徊系プログラマーには泥沼になりかねないのですよ、勉強することが多すぎて。

次回はちょっと思いついた Thread を使った高速化にチャレンジしてみますよ。かえって遅くなるんじゃないかって心配もありますけどね。

ではまた。餅はほどほどにね。

 

You can skip to the end and leave a response. Pinging is currently not allowed.
Subscribe to RSS Feed Twitter は Ume108 だよ