生PCMを、先読み移動平均補間でアップサンプリングしてみた。

#事の始まり
- 今回、自主製作ゲームを作るにあたってSharpDXとNAudio、XAudio2でサウンド周りをポチポチ作ってたら、色々勉強になったのでまとめてみる。
    - そんな感じでやってたら、いつの間にか2chオーディオの5.1chサラウンド化と偽レゾbit化をやってたと言う……
    - NAudioを使っているのは、16bit Wavを読みだすためだけです。(チャンクがどうのこうの、構造体がどうのこうの、趣味のプログラマの私には面倒この上ない。wavファイルを簡単に生PCMにできたらいいのになあ。)

　※なお、ここに書いたソースは何も参考書見ないでやりました。が、他の人とカブっても知らないです。ある程度の人なら誰でも考えつくソースだと思います。
　※打ち込みながらコメント書いたので、ソースに全角空白入ってたらスミマセン。^^;
##以下ソースと説明
###16bit(byte[]) 2ch PCMをfloatにしてLeft/Right分離
- wavファイルの読み出しは他のところを当たってください。^^;
- とりあえず、2chの16bit byte[]型の生PCMをfloat[]型＆Left/Rightにする

```
    public static void flo_wav2(byte[] dat,out float[] da_l,out float[] da_r)
    {
        //byte[]型配列の生PCM(16bit wav/2ch)を
        // float[]型に変換(ついでに左右に分ける/どっちが左でどっちが右だか忘れたのでleft,rightは適当)
        float[] flo1 = new float[dat.Length / 4];
        float[] flo2 = new float[dat.Length / 4];
        for (int i = 0; i < dat.Length; i += 4)
        {
            flo1[i / 4] = bit16_to_flo(dat, i);
            flo2[i / 4] = bit16_to_flo(dat, i + 2);
        }
        da_l = flo1; da_r = flo2;
    }
    
    private static float bit16_to_flo(byte[] byt,int i)
    {
        short inb = BitConverter.ToInt16(byt, i);
        //2の補数で-32768なのが嫌
        if (inb == short.MinValue) { inb++; }
        if (inb != 0) { return (float)((double)inb / short.MaxValue); }
        return 0;
    }

```

###アップサンプリング
- サンプリングレートがdecimal型なので、桁あふれするまでやってくれると思います。(44100hz→192000hzはできた)
- とりあえずPCMの補間割り込み位置にはfloat.MinValueを入れておきます。
- PCMの[0]に最初の音を入れてます。(適当ですが、これをどうするかでプログラム変わる^^;)

```

    public static void flo_up_sampling(ref float[] dat_l,ref float[] dat_r,decimal sample, int hz)
    {
        List<float> mus_l = new List<float>();
        List<float> mus_r = new List<float>();
        decimal ina = sample / (decimal)hz;
        decimal ina_1 = Math.Floor(ina);
        decimal inb = 0;
        for (int i = 0; i < dat_l.Length;i ++)
        {
            //PCM
            mus_l.Add(dat_l[i]); mus_r.Add(dat_r[i]);
            //整数倍
            for (int i2 = 1; i2 < ina_1; i2++)
            { mus_l.Add(float.MinValue); mus_r.Add(float.MinValue); }
            //非整数倍
            inb += ina - ina_1;
            if (inb >= (decimal)1)
            {
                inb -= (decimal)1;
                mus_l.Add(float.MinValue); mus_r.Add(float.MinValue);
            }
        }
        //PCMデータのアライメント？があるみたいなので、調整
        //誰か教えてくださいT_T
        int ck_ck = mus_l.Count % (int)sample;
        if (ck_ck != 0)
        {
            for (int i = 0; i < (int)sample - ck_ck; i++) { mus_l.Add(0); mus_r.Add(0); }
        }
        dat_l = mus_l.ToArray();
        dat_r = mus_r.ToArray();
    }

```

###音の隙間お埋めします。
- 次は捻じ込まれたfloat.MinValueに音を入れる
- PCMの音(現在より次)の位置をインデックス化する
- ma_pには移動平均のベースを入れてください

```

    public static void hokan(ref float[] mus_l,ref float[] mus_r,int ma_p)
    {
        List<int> ind = new List<int>();
        int i2 = 0;
        for (int i = 1; i < mus_l.Length; i++)
        {
            //頭出し
            if (mus_l[i] != float.MinValue) { ind.Add(i); i2 = i; break; }
        }
        for (int i = i2 + 1; i < mus_l.Length; i++)
        {
            //ind[i]に次のPCMの位置が入る
            if (mus_l[i] != float.MinValue) { ind.Add(i); i2 = i; }
            else { ind.Add(i2); }
        }

        //多分iの初期化はint i = 1;の方がいい(この場合の結果は同じだと思う)
        for (int i = 0; i < mus_l.Length; i++)
        {
            //PCMの隙間(float.MinValue)を埋める
            //移動平均を計算
            if (mus_l[i] == float.MinValue)
            {
                mus_l[i] = ma(mus_l, i, ma_p, ind);
                mus_r[i] = ma(mus_r, i, ma_p, ind);
            }
        }
    }

```

###一つ先の移動平均なんて為se(ry
- この移動平均は一つ先読みします(そのため、上でind作った)
- 通常のMAで一つ先読みMAなんてできたら、とんでもないことですよね！

```
 
    private static float ma(float[] ary, int st_i, int ma_p, List<int> ind)
    {
        double ret = 0;
        int st_2 = st_i - (ma_p - 1);
        if (st_2 < 0)
        {
            //st_i(aryのインデックス開始)が0以下の時
            if (st_i <= 0)
            {
                if (ary[0] != float.MinValue) { return ary[0]; }
                return 0;
            }
            //ary[0]を足す
            //ary[0]はPCMデータが前提
            for (int i = st_2; i < 0; i++) { ret += ary[0]; }
            st_2 = 0;
        }
        //ma_p-1個aryを足す(ary[st_i]は無視)
        for (int i = st_2; i < st_i; i++) 
        { 
            if (ind[st_i] < ind[st_i + 1]) { }
            ret += ary[i];
        }
        //今現在の次の音を足す
        //if (ind[i] > ind[st_i]) { }で次の音を探す
        if (st_i < ind.Count) 
        { 
            int val = ind[st_i];
            for(int i = st_i; i < ind.Count; i++)
            {
                if (ind[i] > ind[st_i]) { val = ind[i]; break; }
            }
            ret += ary[val];
        }
        else { ret += ary[ind[ind.Count - 1]]; }
        return (float)(ret / ma_p);
    }

```

###float[] left/float[] Right型のPCMを鳴らせるデータにする
- 16bit byte[]型に戻します
- 32bit float[]で演奏したい人は、ここでちょっと一工夫だね
- unsafeとかmarshal使っても良いんじゃないかな？

```

    public static byte[] byt_wav_flo2(float[] dat_l,float[] dat_r)
    {
        byte[] byt = new byte[dat_l.Length * 4];
        byte[] by_l = null, by_r = null;
        for (int i = 0; i < dat_l.Length * 4; i += 4)
        {
            by_l = flo_to_bit16(dat_l[i / 4]);
            byt[i] = by_l[0]; byt[i + 1] = by_l[1];
            by_r = flo_to_bit16(dat_r[i / 4]);
            byt[i + 2] = by_r[0]; byt[i + 3] = by_r[1];
        }
        return byt;
    }

    private static byte[] flo_to_bit16(float flo)
    {
        return BitConverter.GetBytes((short)((double)flo * short.MaxValue));
    }

```

##ここから先にやったこと

- いつか書くかもだけど、SharpDXのXAudio2で32bit PCM(偽レゾbit)の5.1ch再生にトライしたときのこと
    - 普通にwaveformatextensibleでソースにbit指定すると、byte[]配列の下位16bit？(少なくとも半分)しか反映されず、変なことになる。(解決ずみ)
- サラウンド効果をつけたときのこと
    - 各チャンネルを引いたり足したり割ったりで、チャンネルごとのWav編集した
- 上のソースではシングルタスクだけど、Taskを使ってマルチタスクした
    - Task.Wait( )のフリーズ対策のために、少し特殊なことした。
        - Await/Asyncでconfigureawait(false)して戻り値指定するのが一番いいと思うけど、最近まで書き方知らなかったのでiscompletedで無理やりスレッド管理した。
- (数年ぶりに見直して、c#も変わったなあと。Spanとか便利になりましたね。22/03/06)

　こんなかんじ

##最後に
- ma_p(移動平均の数)に4を指定してアップサンプリングさせてますが、結構良い音出ますよ。
- 家では5.1ch AVアンプでサラウンドを楽しんでいます。(手前味噌ですが、個人的にはDts-Neo6とかDolby-PrIIよりも良い音に聞こえる)

###バグ報告
- MAの先読みインデックスの頭出しを忘れていたので修正しました。(18/01/27)
- PCMのアライメント単位があるみたいで、一応アップサンプリング後の周波数に調整しました。(18/02/08)
- MAの先読みをミスしてたので修正しました。多分これでいい。(22/03/06)

###雑感など
　初投稿です。こんな感じで備忘録的にアップをしていきたいと思います。
　昔はZ80アセンブラとかV30アセンブラやBASICやりましたが、今は趣味で楽しむ程度のプログラマーです。
　どうぞ、お手柔らかにお願いいたします。