【C#】音声を波形化して音声認識ランチャーを作る為の記事


~~内容はどうでもいいからとにかくいいねしておくのがおすすめ~~
そろそろ次のを作ろうという事でとりあえず実装してみます。

問題は自分の音声ではブレが発生するからおそらく一致しないだろうという事で、この辺の曖昧一致実装を数学的に解決しなくてはなりません。
かなり厄介だ....

まずは第一段階として尤も単純な実装で試します。
最低限Windowsで動かせればいいから必要ならPythonとか使う。


OxyPlotを使ってみました。幸運にもTeratailの質問で使ってたのもOxyPlotだったため、これを一部真似る。こういうのがないと相当時間掛かってしまうと思う。


# 目次（長くなったので）

[1.前準備](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#1%E5%89%8D%E6%BA%96%E5%82%99)
[2.音声波形を表示する](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#2%E9%9F%B3%E5%A3%B0%E6%B3%A2%E5%BD%A2%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)
[3.実行結果](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#3%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C)
[ここまでの手順のGIt化](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#%E3%81%93%E3%81%93%E3%81%BE%E3%81%A7%E3%81%AE%E6%89%8B%E9%A0%86%E3%81%AEgit%E5%8C%96)
[4.ハミング窓を掛ける](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#4%E3%83%8F%E3%83%9F%E3%83%B3%E3%82%B0%E7%AA%93%E3%82%92%E6%8E%9B%E3%81%91%E3%82%8B)
[ここまでのGit Branch化](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#%E3%81%93%E3%81%93%E3%81%BE%E3%81%A7%E3%81%AEgit-branch%E5%8C%96)
[5.必要な波形の長さを計算で割り出す](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#5%E5%BF%85%E8%A6%81%E3%81%AA%E6%B3%A2%E5%BD%A2%E3%81%AE%E9%95%B7%E3%81%95%E3%82%92%E8%A8%88%E7%AE%97%E3%81%A7%E5%89%B2%E3%82%8A%E5%87%BA%E3%81%99)
[6.保存された音声波形が入力した波形と一致するかどうか判定](https://qiita.com/EndOfData/items/6ff96a5a7ce190fbf1d6#6%E4%BF%9D%E5%AD%98%E3%81%95%E3%82%8C%E3%81%9F%E9%9F%B3%E5%A3%B0%E6%B3%A2%E5%BD%A2%E3%81%8C%E5%85%A5%E5%8A%9B%E3%81%97%E3%81%9F%E6%B3%A2%E5%BD%A2%E3%81%A8%E4%B8%80%E8%87%B4%E3%81%99%E3%82%8B%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E5%88%A4%E5%AE%9A)


# 1.前準備

**マイクを準備する**　（書き忘れ）
1000円程度のUSBマイクを使っている。

## OXyPlotインストール
NugetでOxyPlot.Wpfを検索していれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/2d7e197d-d7a8-5dfb-959a-423c01762716.png)


### デザイナ上で表示させる設定

以下を参考にして

https://qiita.com/mac8/items/8ca2410ad3c9ce866467

EvryThing(https://www.voidtools.com/) でのインストール後検索結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/c66af904-7ea8-87db-5324-70489d5d8f5e.png)

使い方は窓の社でも見て置いてほしい。インストール時に注意すれば自動的に全てインデックスされる（下記参照）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/848916d4-a5d7-f6b9-1172-821e2a2dd230.png)


判明したパス

```powershell
C:\Users\USER\.nuget\packages\oxyplot.wpf.shared\2.1.2\lib\net45\OxyPlot.Wpf.Shared.dll

C:\Users\USER\.nuget\packages\oxyplot.core\2.1.2\lib\net45\OxyPlot.dll
```



これら2つをやや手間だけど参照先に手動で追加すればいい。検索では出ない。
Visual studio 2022にて。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/114e7d66-2df1-0a1d-7b87-3aa279474547.png)

チェックボックスが外れるバグらしきものがある。

**ビルドする**（書き忘れ）

とりあえず出る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/58d67689-1515-e1bf-1af4-06cd05baf7fa.png)

尚、ツールボックスの表示は```PlotView```である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/822c847a-8a22-9c6b-79bc-a2a6f738575c.png)




# 2.音声波形を表示する

※NAudioもNugetでインストールしておいてください。

個人blogなのだが検証すればするほど驚くほど正確なCodeだと言うのが分かる。真面目な人だったらしい。
 
https://wildpie.hatenablog.com/entry/2014/09/24/000900


https://teratail.com/questions/296462


殆どのCodingははてなblogの実装のままでほぼコンパイルが通ってしまう

>もちろんそのままだとError出まくって使いものにならない。```DataPoint```の時点でエラーが出た。
初心者レベルだとこの時点で躓くところだろう。あくまでWPFを使っているのでその対応をするけど、殆ど修正の必要が無かったのでちょっと驚いたって話。


あとはグラフ表示のノウハウを投入するだけだった。

DeviceNumber について
```C#
  waveIn = new WaveIn()
            {
               //Labelに表示されたDeviceChannelを設定する。
               //0でいいらしい
                DeviceNumber = 0, // Default
            };
```



## 外観

※要素毎にコピペしてください

```xml
  <Grid>
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Left">
            <Button Content="Button" Margin="0,0,0,10"
                                 Height="40" Width="70" Click="Button_Click"/>
            <Button Content="Button" 
                                Height="40" Width="70" Click="Save_Click"/>
        </StackPanel>

        <Label VerticalAlignment="Top"
            Background="AliceBlue" Height="50" Width="700" x:Name="label1" />

        <oxy:PlotView 
             Model="{Binding oxyModel, ElementName=root}"
             Controller="{Binding Controller, ElementName=root}" 
            x:Name="plotView" HorizontalAlignment="Center" Margin="0,111,0,0" VerticalAlignment="Top" Height="270" Width="396"/>

    </Grid>　
```

## 本code

※Codeは**一行ずつ**検証しながらコピペしてください。
※errorは型に注意しながら一つずつ解消していってください。
※幾つかはコメントごと他の記事から持ってきてます。

```C#
using NAudio.Wave;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace VoiceLancherTests
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            InitPlot();

        }

        WaveIn waveIn;
        public PlotController Controller { get; } = new PlotController();

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            for (int i = 0; i < WaveIn.DeviceCount; i++)
            {
                var deviceInfo = WaveIn.GetCapabilities(i);
                label1.Content = string.Format("Device {0}: {1}, {2} channels",
                i, deviceInfo.ProductName, deviceInfo.Channels);
            }
            

            waveIn = new WaveIn()
            {
                DeviceNumber = 0, // Default
            };
            waveIn.DataAvailable += WaveIn_DataAvailable;
            waveIn.WaveFormat = new WaveFormat(sampleRate: 16000, channels: 1);
             ///sampleRateは音声認識用の波形に必要なherz数 * 1000

            waveIn.StartRecording();           
        }

        private void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
        {
            // 32bitで最大値1.0fにする（元記事のママ）
            ///float型にしつつ最大1.0fに制限する
            for (int index = 0; index < e.BytesRecorded; index += 2)
            {
                short sample = (short)((e.Buffer[index + 1] << 8) | e.Buffer[index + 0]);
             ///'<<'は左シフト演算子である（初めて見た）
             ///e.Bufferはbyte型の配列
             ///8 ビットシフトすると2の8乗を掛け算した数値になる
             ///なぜ8ビットシフトなのか？16bit Shiftにしたら波形が潰れた（細かすぎ）

                float sample32 = sample / 32768f;
                               ///'f'はfloatを表す
                             ///32bit Floatと関連？
                            ////short型の下限（あるいは上限）で割る
////16ビット： -32,768 ～ 32,767
/// https://pspunch.com/pd/article/bit_depth/
                ProcessSample(sample32);                
            }
        }
        List<float> _recorded = new List<float>(); // 音声データ

        private LineSeries _lineSeries = new LineSeries();
        // OxyPlotのためのモデルとコントローラー
        public PlotModel oxyModel { get; } = new PlotModel();
        // 軸の設定
        public OxyPlot.Axes.LinearAxis AxisX { get; } = new OxyPlot.Axes.LinearAxis();
        public OxyPlot.Axes.LinearAxis AxisY { get; } = new OxyPlot.Axes.LinearAxis();

        private void ProcessSample(float sample)
        {                  
            _recorded.Add(sample);

            if (_recorded.Count == 1024)
            {
                var points = _recorded.Select((v, index) =>
                        new DataPoint((double)index, v)
                    ).ToList();

                _lineSeries.Points.Clear();
                _lineSeries.Points.AddRange(points);
                
                plotView.InvalidatePlot(true);

                _recorded.Clear();
            }        

        }
        void InitPlot()
        {
            var model = new PlotModel();

              model.Axes.Add(new LinearAxis { Minimum = -0.1, Maximum = 0.1, Position = AxisPosition.Left, });
            model.Series.Add(_lineSeries);
            plotView.Model = model;
        }

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            waveIn.StopRecording();

            ///Save recoded Data to CSV            
        }
    }
}
```

### なぜ8ビットシフト変換するのかについてのChatGPTによる解説
実在するドキュメントは分かり次第記載します。

> バイト配列で受信した音声データを**16ビットのショート型に変換するため**に必要です。左シフト演算は、バイト型の下位ビットをショート型の上位ビットに移動させることによって、8ビットから16ビットへの変換を実現します。そして、その後、short型のサンプルを-32768から32767の範囲の浮動小数点数に正規化するために、32768で割る処理が行われています。これにより、入力音声が-32768から32767の範囲で表されるショート型データであった場合でも、**正規化された出力が-1.0から1.0の範囲で表されるように**なります。



# 3.実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/6384110e-c502-5800-0861-1c6b17bb50f7.png)

意外とCodingの難度は低め。
Chart表示までのノウハウが分からないと苦労したと思う。

### chartが表示されない場合

OxyPlotがない。
マイクが正常に接続されてない。
マイクが他のアプリケーションによって排他制御されている
```PlotView```の初期化が出来ていない
羊チューバーの動画を観ていない

この辺りを一つ一つチェックしましょう。

#### 波形データがちゃんと来ているか確認する。
ブレークポイントを活用しましょう。
その行の時点では```points```はnullのままです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/c124279a-4d7c-7fd3-9272-be5a00c1401f.png)



## ここまでの手順のGIt化

プロジェクトファイルですが、Visual Studio2022 .net7を使っています。
今後もなるべく最新の環境を使います。古くなったらリクエスト次第で更新しておきます。
 
https://github.com/Sheephuman/VoiceLancherTests


```powershell
 git Clone https://github.com/Sheephuman/VoiceLancherTests.git
```
URLだけで試したけどやっぱり.git必要だった件。


この後の手順をBranchして分けるなどの施策を試みようと思う。


# 4.ハミング窓を掛ける
**ハミング窓？なにそれ美味しいの？** 的な感じで不要だろうと思って要れなかったんですが、調べたら音声解析を容易にするものらしく、目指す仕様に向けて必要な実装だろうという事でこれを追加します。

詳細な説明

https://www.logical-arts.jp/archives/124

場合によっては両端の要素がゼロになるという**ハニング窓**を使うべきかも知れない。

Math.NET NumericsをNugetでインストールします。

Haming窓表示用のChartを別に用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/d6881ba7-62fc-9a74-55da-fc98c322dbac.png)

### 初期化用のInitPlotを引数を取るものに改造します。


```C#:void InitPlot
   void InitPlot(LineSeries _lineSeries, PlotView _plotView )
            {
                var model = new PlotModel();


       ///内部コントロール判定
           if (_plotView.Name == hammingPlotView.Name)
            {
                model.Axes.Clear();
                model.Axes.Add(new LinearAxis { Minimum = 0, Maximum = 10000, Position = AxisPosition.Bottom });
                model.Axes.Add(new LinearAxis { Minimum = 0, Maximum = 4, Position = AxisPosition.Left, });
            }

                model.Series.Add(_lineSeries);
                _plotView.Model = model;
}

////コンストラクタ
 public MainWindow()
            {
                InitializeComponent();

                InitPlot(_lineSeries,plotView);
                InitPlot(_hammingLineSeries, hammingPlotView);
            }

```

### Hamming窓用のSeries（系列）とモデルを用意します。

```c#
 ////Field for Haming Window 
 private LineSeries _hammingLineSeries = new LineSeries(); 
 public PlotModel hammingOxyModel { get; } = new PlotModel();
```

### Hamming窓用のメソッドを作成します

質問するまでもなくかなり簡単だった。
コメントによるとWindowSizeとは波形データ```float<List>```のカウント（要素数）を指すらしい


```C#

 List<DataPoint> HammingMethod()
        {
           var window = MathNet.Numerics.Window.Hamming(_recorded.Count);
　　　        ///そのままだとWinFormとの曖昧参照を起こす（めんどい）
　
           var haming_recorded = _recorded.Select((v, i) => v * (float)window[i]).ToList();

            Complex[] complexData = haming_recorded.Select(v => new Complex(v, 0.0)).ToArray();
           ///複素数配列データに変換

            Fourier.Forward(complexData, FourierOptions.Matlab); // arbitrary length                             
           /// フーリエ変換した処理を返す

            var s = haming_recorded.Count * (1.0 / 16000.0);
        ///要素数に（1 / Hertz数）を掛ける（この場合は16kHz）
        ///これをしないとDataPointに必要な解像度を得られず、波形が潰れる

            var point = complexData.Take(complexData.Count() / 2).Select((v, index) =>
                  new DataPoint((double)index / s,
            Math.Sqrt(v.Real * v.Real + v.Imaginary * v.Imaginary))
            ).ToList();
        ////波形データの平方根を計算
        ////Enumerable.Take メソッド (System.Linq) 
　      ////シーケンスの先頭から要素を返す

            this.hammingPlotView.InvalidatePlot(true);            return point;
        }
```
 これをProcessSampleメソッド内で実行します。

```C#
  private void ProcessSample(float sample)
        {
            _recorded.Add(sample);
            if (_recorded.Count == 1024)
　           {////省略}           
                _lineSeries.Points.Clear();
                _hammingLineSeries.Points.Clear();

                _lineSeries.Points.AddRange(points);
                _hammingLineSeries.Points.AddRange(HammingMethod());

                plotView.InvalidatePlot(true);
                hammingPlotView.InvalidatePlot(true);

                _recorded.Clear();
            }
        }
```


## ここまでの実行結果

https://www.youtube.com/watch?v=zJRHpMI2-60
 
## ここまでのGit Branch化

以前のCodeは
```git clone https://github.com/Sheephuman/VoiceLancherTests.git```
でそのまま取得できます。

```powershell:Git
git clone -b HammingWindow_Branch https://github.com/Sheephuman/VoiceLancherTests.git
```


# 5.必要な波形の長さを計算で割り出す

 これは単なる計算の問題で、多分大学入れる人程度ならすぐ計算出来ると思うんだけどまあとりあえず調べて割り出す。カネが掛からなくていい。

 まず実際に録音に必要な秒数と音声認識に適したサンプルレートから、必要なデータの長さを割り出す。録音に使用するデータはストップウォッチで計測したところ2秒～5秒といったところ（対象のアプリケーション名かその一部を呼べばいい）

音声認識に適したサンプルレートは16khzで十分とのこと。
**※後日より精度を上げる為に32khzに変えたりしてます。**


https://amivoice-tech.hatenablog.com/entry/2022/11/29/105246


>　16(khz) * 1000 * 5(秒) = 80000（plot数）

この数に対して2のべき乗に最も近い自然数を割り出す（高速フーリエ変換-FFTは2のべき乗でないと出来ないという制限がある　-参考：https://cognicull.com/ja/f5q2jl62
　ChatGPTのWeb版みたいな出典不明の謎サイト　）

https://toricor.hatenablog.com/entry/2016/02/20/005017

関数電卓にも慣れたかったが使い方がよく分からなかった。

適当にC#に変換して
```C#
 private void Calc_Click(object sender, RoutedEventArgs e)
        {
            int n = 80000;
            string bin = Convert.ToString(n, 2); 
            int len = bin.Length;
            Debug.WriteLine(len - 1); 
        }
```
結果 : 16

以上から2の16乗ないし17乗を使用すればよいことになる。


得られた数は65536（5秒未満）である。

~~これほどの要素数となるとかなりchart表示が重くなってしまうが~~　重いというより割り出した波形の長さである4秒~5秒弱毎になってるらしい。音声認識ランチャーを作るのが目的なので問題ないだろう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/7228dcda-f393-be18-47c1-60ac3a4d2d67.png)



## CSV化

結構便利な記事があったんで載せとく。非同期メソッドとかIDisposableまで実装してあるのが用意がいい。

https://garafu.blogspot.com/2014/10/c-list-csv.html





# 6.保存された音声波形が入力した波形と一致するかどうか判定

満を持しての更新。野良仕事と体調不良でなかなか作業出来なかった。
冒頭にも書いたけど一致は（多分）100％しません。一致率を数値として出すものを見つけたのでそれを試してみようという項です。

wildpieの日記(https://wildpie.hatenablog.com/entry/2014/10/13/122909 )にありました。感謝しかないですね。多分ここぐらいしかないので。

## 実行結果の記録記事

記事内で要素数を絞り込む対策を施しました（宜しければストックしてください）。

https://qiita.com/EndOfData/items/1db69fd4b0b5e1869e80　

一応**成功**してますので、これで次回は指定のアプリケーションを起動させるとこまでは行けます。このままだとノイズ等の影響でMinDistance結果が安定しないようなので、次回更新ではローパスフィルタの実装を試みる予定です（いいねとか増えない場合はblog等に転載させて頂きます(笑)）。

## Gir Branch化
   実はリポジトリが以前のまま更新されてなかったので（当然作業内容は無に帰した）、適切な操作方法を覚えたうえでComitし直しました。陳謝。

```powershell
git clone -b  BPMatching_Brunch https://github.com/Sheephuman/VoiceLancherTests.git 
```
test済みです。
ここではCSV保存はまだ行わず、ヒープに確保されたListの配列とそのまま比較しています。
~~問題点は計算に時間が掛かり過ぎる（**Ryzen7 3700xで35秒かかる**）事で、改善を試みているところです。~~
→6秒前後まで短縮。
コメント歓迎。
Pythonで行う方法は**いいねが増えたら**試します。


# 波形にフィルタを掛ける
 出力結果が安定しないのでローパスフィルタを掛けます。
 



# 波形を保存してデータベース化する

まだこれといった手段やアイディアはないんですが、ファイルサイズがかなり肥大化する事が予想されます。
これに対する施策として、データベース登録時にidなどを付加してそのままLinkする方式を取る事にします。これによりデータベースの全波形を照合などという事は避けられます。