PrismWPFSample(7)OxyPlot

Prismを使用したWPFアプリケーション開発で役に立つと思われる項目を一つのアプリケーションにまとめたものを作りました。今回は、OxyPlotについて書いています。

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

アプリの外観はこんな感じです。

f:id:feynman911:20190628181443j:plain


OxyPlot の使い方については以前書いてますので、このサンプルの内容を中心に解説します。
feynman.hatenablog.com

サンプルの動作

OxyPlot のサンプルは Module4 になります。
PCのマイク入力から音声を取り込んで左側の Chart に波形表示、右側の Chart にFFT波形を表示します。
ですので、PCにマイクを差したうえで動作させてください。
あと、 Chart 上で右clickした時に ContextMenu を表示するようにしてあります。
ContextMenu としては、

  • 自動リサイズ:データ全体が表示されるようにフィッティング
  • 保存:Chartを画像としてファイル保存
  • コピー:Chartをクリップボードにコピー

の3つとなります。
OxyPlot の Chart は標準で、以下の機能が組み込まれています。

  • マウスホイールでの拡大縮小
  • Chart上、左クリックでポイント座標の表示
  • 右ドラッグで表示位置の移動

拡大縮小は、X軸上でX軸の拡大縮小 Y軸上でY軸の拡大縮小 Chart内で全体の拡大縮小が可能です。
しかしながら 一度拡大縮小を行ってしまうと、データが新しくなった時もデータに合わせた表示範囲の変更をしなくなってしまうので、それを標準状態に戻すために自動リサイズという ContextMenu を追加しています。

ContextMenu用デリゲートコマンドの追加

Chart を2つ表示して、どちらにも同じ ContextMenu を表示したいので、パラメータ付きのデリゲートコマンドを使用します。
Prism Template Pack で導入されているスニペットから generic タイプを選択して記述します。

f:id:feynman911:20190722123745j:plain

private DelegateCommand<PlotModel> commandResizePlot;
/// <summary>
/// 指定されたプロットモデルの表示範囲をリセット
/// Plot1 or Plot2
/// </summary>
public DelegateCommand<PlotModel> CommandResizePlot =>
    commandResizePlot ?? (commandResizePlot = 
   new DelegateCommand<PlotModel>(ExecuteCommandResizePlot));
void ExecuteCommandResizePlot(PlotModel parameter)
{
    AutoResizePlot(parameter);
}

public void AutoResizePlot(PlotModel pm)
{
    pm.ResetAllAxes();
    pm.InvalidatePlot(true);
}

View からパラメータとして対応する PlotModel を受け取って、それに対して処理を行うという流れになります。
Chart の保存とコピーの処理も同じようにして下記になります。(デリゲートコマンドは省略)

public void SavePlot(PlotModel pm, string filename = "", 
             int width = 800, int height = 600)
{
    if ( filename == "")
    {
        var dlg = new SaveFileDialog
        {
            Filter = ".png files|*.png|.pdf files|*.pdf",
            DefaultExt = ".png"
        };
        if (dlg.ShowDialog().Value) filename = dlg.FileName;
    }
    if (filename != "")
    {
        var ext = Path.GetExtension(filename).ToLower();
        switch (ext)
        {
            case ".png":
                using (var s = File.Create(filename))
                {
                    Application.Current.Dispatcher.Invoke((Action)(() =>
                    {
                        OxyPlot.Wpf.PngExporter.Export(
           pm, s, width, height, OxyColors.White);
                    }));
                }
                break;
            case ".pdf":
                using (var s = File.Create(filename))
                {
                    Application.Current.Dispatcher.Invoke((Action)(() =>
                    {
                        OxyPlot.PdfExporter.Export(pm, s, width, height);
                    }));
                }
                    break;
            default:
                break;
        }
    }
}
public void CopyPlot(PlotModel pm, int width = 800, int height = 600)
{
    var pngExporter = new OxyPlot.Wpf.PngExporter 
  { Width = width, Height = height, Background = OxyColors.White };
    Application.Current.Dispatcher.Invoke((Action)(() => 
    {
        var bitmap = pngExporter.ExportToBitmap(pm);
        Clipboard.SetImage(bitmap);
    }));
}

ContextMenu の追加方法

PlotView に ContextMenu を追加するには次のようにします。

PlotView を選択
Property の ContextMenu の新規作成を押して ContextMenu を作成します。

f:id:feynman911:20190722130253j:plain

追加された ContextMenu にある Items のボタンを押します。

f:id:feynman911:20190722130520j:plain

開いたコレクションエディターのダイアログで MenuItem を3つ追加します。

f:id:feynman911:20190722130639j:plain


それぞれの MenuItem で追加するのは Command と CommandParameter と Header になります。
Command はViewModel のデリゲートコマンドにバインドします。

f:id:feynman911:20190722132834j:plain

CommandParameter は、操作する ViewModel この場合には Plot1(選択した PlotView とバインドする ViewModel)にバインドします。

f:id:feynman911:20190722133054j:plain

Header は ContextMenu に表示される文字となるので、多言語化を考慮して次のようにしておくと、言語切り替えが可能となります。

f:id:feynman911:20190722191440j:plain

表示される文言はResX Manager で CommonModels のResources に書いておきます。

f:id:feynman911:20190722191912j:plain

作成されるXAMLは次の様なものになります。

<oxy:PlotView x:Name="PlotView1" Model="{Binding Plot1}" Margin="0,0,5,0">
       <oxy:PlotView.ContextMenu>
            <ContextMenu>
                 <MenuItem Header="{lex:Loc AUTORESIZE}"
                       Command="{Binding CommandResizePlot, Mode=OneWay}" 
                       CommandParameter="{Binding Plot1}"/>
                  <MenuItem Header="{lex:Loc SAVE}" 
                       Command="{Binding CommandSavePlot, Mode=OneWay}" 
                       CommandParameter="{Binding Plot1}"/>
                 <MenuItem Header="{lex:Loc COPY}" 
                       Command="{Binding CommandCopyPlot, Mode=OneWay}"
                       CommandParameter="{Binding Plot1}"/>
          </ContextMenu>
      </oxy:PlotView.ContextMenu>
</oxy:PlotView>

音声キャプチャ

音声キャプチャ用のライブラリーとしては NAudio を使用しています。
github.com

//デバイスの抽出
for (int i = 0; i < WaveIn.DeviceCount; i++)
{
    var deviceInfo = WaveIn.GetCapabilities(i);
    commondata.StatusString = String.Format("Device {0}: {1}, {2} channels",
        i, deviceInfo.ProductName, deviceInfo.Channels);
}
//デバイス0のサンプリングをスタート
waveIn = new WaveIn()
{
    DeviceNumber = 0, // Default
};
waveIn.DataAvailable += WaveIn_DataAvailable;
waveIn.WaveFormat = new WaveFormat(sampleRate: 8000, channels: 1);
waveIn.StartRecording();
private void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
{
    // 32bitで最大値1.0fにする
    for (int index = 0; index < e.BytesRecorded; index += 2)
    {
        short sample = (short)((e.Buffer[index + 1] << 8) | e.Buffer[index + 0]);

        float sample32 = sample / 32768f;
        Capture(sample32);
    }
}

float[] SoundData = new float[1024]; // 音声データ
private void Capture(float sample)
{
    SoundData[count] = sample 
            * (float)FastFourierTransform.HammingWindow(count,1024);
    count++;
    if (count == 1024)
    {
        MakeFFT(SoundData);
        plotModelFFT.InvalidatePlot(true);
        count = 0;
    }

    DataPointsRT.Add(new DataPoint(counttime, sample));
    if (DataPointsRT.Count > 1024) DataPointsRT.RemoveAt(0);
    plotModelRT.InvalidatePlot(true);
    counttime = counttime + 1.0 / 8000.0;
}

FFT処理

FFT処理のライブラリーは色々とありますが、速度比較、ライセンス等を考えると NAudio が使いやすいように思います。

www.codeproject.com

ここでも、NAudio を使用しています。

public void MakeFFT(float[] data)
{
    var fft = ExecuteFFT(data);
    DataPointsFFT.Clear();
    for (int i = 0; i < 512; i++)
    {
        DataPointsFFT.Add(new DataPoint(i * 4000.0 /512.0, fft[i]));
    }
}

public float[] ExecuteFFT(float[] data)
{
    int len = data.Count();
    int m = (int)Math.Log((double)data.Count(), 2);
    var fftSample = data.Select(v => new Complex { X = v, Y = 0.0f }).ToArray();
    FastFourierTransform.FFT(true, m, fftSample);
    var ret = new float[len / 2];
    for( int i=0 ; i<len/2; i++)
    {
        ret[i] = (float)Math.Sqrt(fftSample[i].X * fftSample[i].X 
                + fftSample[i].Y * fftSample[i].Y) * 2.0f;
    }
    return ret;
}


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

次回は、8. Oxyplot の Heatmap を使ったスペクトグラム表示 について記述したいと思います。