PrismWPFSample(8)OxyPlot-Heatmap

Prismを使用したWPFアプリケーション開発で役に立つと思われる項目を一つのアプリケーションにまとめたものを作りました。今回は、OxyPlotの Heatmap を使用してスペクトグラムの表示を追加したので紹介します。

動作環境:Win10, Visual Studio Community 2017, Prism V7.1.0.431, .NET4.5.2, Prism Template Pack, TraceListeners, WPFLocalizeExtension, OxyPlot

前回の内容にスペクトログラム表示を追加してアプリの外観はこんな感じです。

f:id:feynman911:20190729002142j:plain

DispatcherTimer

前回は音声データをキャプチャーするタイミングで表示も更新していましたが、今回はキャプチャーと表示を分離しました。音声取得のコールバックではデータのバッファリングのみを行い、表示は DispatcherTimer で一定時間ごとに行うように変更しています。
DispatcherTimer は、UIを操作する為のタイマーで、次のような特徴を持っていて、非常に簡単に使う事が出来ます。

  • UIスレッドで動作するので、そのまま画面にアクセスする事ができる。
  • 再入対策が不要。(処理が終わっていなければ次の割り込みは発生しない)
  • 処理のプライオリティー設定が可能。

タイマーは2つ用意して、波形表示用とFFT&スペクトグラム表示用に分けています。
右側のチャートは 周波数スペクトルとスペクトグラムをラジオボタンを使って切り替えられるようにしています。

private DispatcherTimer mytimer1;
private DispatcherTimer mytimer2;

/// <summary>
/// タイマーの初期化
/// </summary>
private void SetupTimer()
{
    mytimer1 = new DispatcherTimer();
    mytimer2 = new DispatcherTimer();

    // 優先度はDispatcherPriority.Background

    // インターバルを設定
    mytimer1.Interval = TimeSpan.FromMilliseconds(100);
    mytimer2.Interval = TimeSpan.FromMilliseconds(100);
    // タイマメソッドを設定
    mytimer1.Tick += timer_Tick1;
    mytimer2.Tick += timer_Tick2;
}

void timer_Tick1(object sender, EventArgs e)
{
    MySoundModel.PlotRT();
}
void timer_Tick2(object sender, EventArgs e)
{
    if (MyType == EnumDefines.RBEnum.RB1) MySoundModel.PlotFFT();
    if (MyType == EnumDefines.RBEnum.RB2) MySoundModel.PlotSpectrogram();
}
/// <summary>
/// スペクトログラム表示
/// </summary>
public void PlotSpectrogram()
{
    float[] data = new float[fftnum];
    Array.Copy(SoundData, data, fftnum);
    AddSpect(ExecuteFFT(data));
    PlotModelSpectrogram.InvalidatePlot(true);
}

Heatmap

Heatmapを使う為には、XYの座標用の LinearAxis2本と、色表示用の LinearColorAxis 1本を配置します。
DispatcherTimerの起動ごとに fftnum 個ずつFFT処理されたデータを AddSpect(float[] data) でシフトしながら2次元配列に蓄積し、それを一気に double の2次元配列にコピーして HeatMapSeries.Data に設定します。
OxyPlotで使用しているデータを直接編集するのではなく、別途用意して置き換えるという方が動作が軽いように思います。
Chartの表示座標は2次元配列の Index になるので、表示用の LinearAxis2本は軸の表示を行わず、目盛りを表示するためにダミーの軸を追加しています。

private PlotModel plotModelSpectrogram = new PlotModel();
/// <summary>
/// FFT表示用プロットモデル
/// </summary>
public PlotModel PlotModelSpectrogram
{
    get { return plotModelSpectrogram; }
    set { SetProperty(ref plotModelSpectrogram, value); }
}

float[,] Data = new float[100, fftnum / 2];
public HeatMapSeries heatMapSeries1 = new HeatMapSeries();

/// <summary>
/// スペクトログラムチャートの初期化
/// </summary>
public void InitPlotModelSpectrogram()
{
    PlotModelSpectrogram.Title = "Spectrogram";
    PlotModelSpectrogram.PlotMargins = new OxyThickness(50.0, 0.0, 50.0, 40.0);

    var linearColorAxis1 = new LinearColorAxis();
    linearColorAxis1.HighColor = OxyColors.Gray;
    linearColorAxis1.LowColor = OxyColors.Black;
    linearColorAxis1.Position = AxisPosition.Right;
    linearColorAxis1.Key = "z";
    linearColorAxis1.Minimum = 0.0;
    linearColorAxis1.Maximum = 0.01;
    linearColorAxis1.AbsoluteMinimum = 0.0;
    linearColorAxis1.AbsoluteMaximum = 0.01;
    PlotModelSpectrogram.Axes.Add(linearColorAxis1);

    var linearAxis1 = new LinearAxis();
    linearAxis1.Position = AxisPosition.Bottom;
    linearAxis1.Key = "x";
    linearAxis1.IsAxisVisible = false;
    linearAxis1.IsZoomEnabled = false;
    PlotModelSpectrogram.Axes.Add(linearAxis1);

    //目盛り表示用ダミー
    var linearAxis_a = new LinearAxis();
    linearAxis_a.Position = AxisPosition.Bottom;
    linearAxis_a.Key = "a";
    linearAxis_a.Minimum = -10.0;
    linearAxis_a.Maximum = 0;
    linearAxis_a.Title = "Time";
    linearAxis_a.Unit = "sec";
    linearAxis_a.IsZoomEnabled = false;
    PlotModelSpectrogram.Axes.Add(linearAxis_a);

    var linearAxis2 = new LinearAxis();
    linearAxis2.Position = AxisPosition.Left;
    linearAxis2.Key = "y";
    linearAxis2.IsAxisVisible = false;
    linearAxis2.IsZoomEnabled = false;
    PlotModelSpectrogram.Axes.Add(linearAxis2);

    //目盛り表示用ダミー
    var linearAxis_b = new LinearAxis();
    linearAxis_b.Position = AxisPosition.Left;
    linearAxis_b.Key = "b";
    linearAxis_b.Minimum = 0.0;
    linearAxis_b.Maximum = 4000.0;
    linearAxis_b.Title = "Frequency";
    linearAxis_b.Unit = "Hz";
    linearAxis_b.IsZoomEnabled = false;
    PlotModelSpectrogram.Axes.Add(linearAxis_b);

    heatMapSeries1.Data = new double[100, fftnum / 2];
    heatMapSeries1.X0 = 0.0;
    heatMapSeries1.X1 = 100.0;
    heatMapSeries1.Y0 = 0.0;
    heatMapSeries1.Y1 = fftnum / 2;

    PlotModelSpectrogram.Series.Add(heatMapSeries1);
    //heatMapSeries1.Interpolate = false;
}

/// <summary>
/// 振幅データをスペクトログラム配列に追加する
/// </summary>
/// <param name="data"></param>
private void AddSpect(float[] data)
{
    for (int i = 0; i < 99; i++)
    {
        for (int j = 0; j < fftnum / 2; j++)
        {
            Data[i, j] = Data[i + 1, j];
        }
    }
    for (int j = 0; j < fftnum / 2; j++)
    {
        Data[99, j] = data[j];
    }

    var pldata = new double[100, fftnum / 2];
    Array.Copy(Data, pldata, Data.Length);
    heatMapSeries1.Data = pldata;
}


作成したサンプルは次の場所に置いてありますので、詳しくはソースコードを見てもらえればと思います。
github.com

次回は、9. 2重起動禁止 について記述したいと思います。