C#
wav
移動平均
PCM
アップサンプリング

生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 < 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なんてできたら、とんでもないことですよね!
  • 二つ以上先を読みだすためには、if (ind[st_i] < ind[st_i + 1]) { } などで先読みさせるといいよ
    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++) { ret += ary[i]; }
        //今現在の次の音を足す
        //ary[ind[st_i]]の内容はary[st_i]の次の音
        if (st_i < ind.Count) { ret += ary[ind[st_i]]; }
        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で無理やりスレッド管理した。

 こんなかんじ

最後に

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

バグ報告

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

雑感など

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