2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2018-01-26

#事の始まり

  • 今回、自主製作ゲームを作るにあたって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やりましたが、今は趣味で楽しむ程度のプログラマーです。
 どうぞ、お手柔らかにお願いいたします。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?