#事の始まり
- 今回、自主製作ゲームを作るにあたって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で無理やりスレッド管理した。
- Task.Wait( )のフリーズ対策のために、少し特殊なことした。
- (数年ぶりに見直して、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やりましたが、今は趣味で楽しむ程度のプログラマーです。
どうぞ、お手柔らかにお願いいたします。