SVX日記
2026-05-18(Mon) 空間音響、または、ドップラー効果シミュレータ
耳はふたつあるので、音の大きさに多少の差が生じているであろうことはすぐに思いつくが、耳はふたつしかないので、それだと「左右」はともかく「前後」「上下」が知覚できる理由にはならない。で、調べていくと、耳の形状がそれに寄与しているらしい。耳は後方のみに張り出した形状だ。前方からの音はそのまま鼓膜に届くが、後方からの音は耳のエラ(?)の部分を回り込む必要があるので、音量も到達時刻も音質も微妙に変化する。それを検出しているらしい。なるほど。そういわれれてみると「左右」でも、耳の位置は違うし頭自体を回り込む必要もあるから到達時刻の差は生じているはずだな。音は意外と遅い。
臨場感の高める録音方法としてバイノーラル録音というものがある。耳の形状まで再現された人の頭の模型を用意して、左右の耳の穴の中に設置されたマイクで録音する手法だ。それで収録すれば、声優の立ち位置の変化などがちゃんと結果に反映される。
簡単なプログラムでは耳や頭の形状までシミュレートすることは難しいが、音量と到達時刻だけなら三角関数で算出可能だ。それなら少なくとも「左右」については知覚可能になるのではないか。ちょっとコードを書いてみるか。
class Man
# 自分の位置、向き、耳の間の距離(m)
def initialize(x = 0.0, y = 0.0, d = 0.0, w0 = 0.17)
@x = x; @y = y; @d = d; @w = w0 / 2
@sources = []
end
# 耳の位置を算出して返す
def ear_position
r = @d * PI / 180
lx = @x - cos(r) * @w; ly = @y - sin(r) * @w # 左耳
rx = @x + cos(r) * @w; ry = @y + sin(r) * @w # 右耳
[[lx, ly], [rx, ry]]
end
# 音源オブジェクトを追加
def <<(source)
@sources << source
end
# 指定の時刻間の音をレンダリング
def rend(base_wav, t0 = 0, t1 = 1, file = 'output.wav')
wav = NewWavFile.new(base_wav)
wav.get_info[0].each {|l|
puts(l)
}
vol = 30000 * (5 ** 2) # 音量係数(5mの位置で±30000)
t0 *= 44100; t1 *= 44100; (t0...t1).each {|t|
yield(t)
ear_pos = ear_position
l_gain = r_gain = 0; @sources.each {|source|
src_pos = source.position(t)
l_dist = sqrt((src_pos[0] - ear_pos[0][0]) ** 2 + (src_pos[1] - ear_pos[0][1]) ** 2)
r_dist = sqrt((src_pos[0] - ear_pos[1][0]) ** 2 + (src_pos[1] - ear_pos[1][1]) ** 2)
l_time = l_dist * 44100 / 340.290 # 音速(340.29m/s)による遅れ(1/44100s単位)
r_time = r_dist * 44100 / 340.290
l_gain += source.get_gain(t - l_time) * (vol / l_dist ** 2) # 過去の発生音量(ゲイン)、距離減衰を加味
r_gain += source.get_gain(t - r_time) * (vol / r_dist ** 2)
}
wav << [l_gain, r_gain]
}
wav.save_phrase(0, t1 - t0, file)
end
end
class SoundSource
def initialize(file = nil)
file and @wav = NewWavFile.new(file)
end
# 時刻 t の時点の位置を返す、(0, 0) から 10m の位置を 4 秒で左回りに周回する
def position(t)
dist = 10
d = t.to_f * 90 / 44100
r = d * PI / 180
[-dist * sin(r), dist * cos(r)]
end
# 音(波)を発生
def get_gain(t)
g0 = get_gain0(t0 = t.floor)
g1 = get_gain0(t0 + 1)
g0 + (g1 - g0) * (t - t0) # 線形補間
end
def get_gain0(t)
(@wav.get_gain(t)[1] || 0).to_f / 32768
end
end
メインプログラムはこう。音のサンプルは以前に自作した効果音生成ツールで作ったパックマンのモンスタが歩く音だ。
man = Man.new(0, 0)
ss = SoundSource.new('pacmon.wav')
man << ss
man.rend(ARGV[0], 0, 10) {|t|
}
これでwavファイルが生成される<聴いてみる>。おぉッ! アカベイが自分の回りをグルグルと回っているッ! 「10m前方から左回り」という設定だが、確かにそう聴こえる。実際には前後は再現できていないのだけれど。
……ん? と、ここで気がついた。時間に対して位置の変化が発生する場合、いわゆる救急車の「ピーポーピーポーヘーホーヘーホー」というドップラー現象まで再現されてしまうのではないだろうか? SoundSourceクラスを継承し、サイレンを鳴らしながら直線移動する音源を実装する。
class Ambulance < SoundSource
# 時刻 t の時点の位置を返す、時速 60km (100m 前方から、100m 後方まで 12 秒)で、右 5m の位置を通り過ぎる
def position(t)
t1 = t % (12 * 44100)
[5, 100 - t1.to_f * 200 / 44100 / 12]
end
# 音(波)を発生
def get_gain(t)
f = t % 52920 > 26460 ? 770.0 : 960.0
omega = 2 * PI * f / 44100
sin(omega * t)
end
end
こんなwavファイルが生成された<聴いてみる>。思った通りッ! 救急車が前方から走ってきて対向車線をすれ違ったッ! 「100m前方から右5mを通り過ぎる」という設定だが、確かにそう聴こえる。ドップラー現象もバッチリ再現できている。やっぱり実際には前後の違いは再現できていないのだけれど。
他に「すれ違うドップラーな移動物体」はないなかぁ、と思って思いついたのが街宣車。SoundSourceクラスを継承し、楽曲を鳴らしながら直線移動する音源を実装する。同時に2台を走らせたいので、ちょっと遅れて走ってくるためのコードも追加する。
こんなwavファイルが生成された<聴いてみる>。時々あるシチュエーションが再現できているように思える。まぁ、実際には2台が同時に楽曲を鳴らしていることはないだろうけれど。
こんなwavファイルが生成された<聴いてみる>。名鉄ファンには堪えられない状況ではないか。列車が引き起こす風圧まで感じるほどだ。際には風圧は再現できていないのだけれど。
つうわけで、シンプルなコードで割とリアルなサウンドを再現できた気がする。最近はFPS的なゲームが多く、3D空間でキャラクタを動かすものが多いから、敵の場所を察知させるためにサウンドにはコダわっていると思うのだが……はて? こういうリアルさを感じたことがないな(いや、手元のゲーム機が壊れかけてから最近のゲームをしてないけれど)。
