SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|12|
2025|01|02|03|04|05|06|07|08|09|10|11|12|
2026|01|02|03|04|05|

2026-05-18(Mon) 空間音響、または、ドップラー効果シミュレータ

  ちょっと前に「人が音の聴こえる方向を知覚できる」という現象に興味を持った。

  耳はふたつあるので、音の大きさに多少の差が生じているであろうことはすぐに思いつくが、耳はふたつしかないので、それだと「左右」はともかく「前後」「上下」が知覚できる理由にはならない。で、調べていくと、耳の形状がそれに寄与しているらしい。耳は後方のみに張り出した形状だ。前方からの音はそのまま鼓膜に届くが、後方からの音は耳のエラ(?)の部分を回り込む必要があるので、音量も到達時刻も音質も微妙に変化する。それを検出しているらしい。なるほど。そういわれれてみると「左右」でも、耳の位置は違うし頭自体を回り込む必要もあるから到達時刻の差は生じているはずだな。音は意外と遅い。

  臨場感の高める録音方法としてバイノーラル録音というものがある。耳の形状まで再現された人の頭の模型を用意して、左右の耳の穴の中に設置されたマイクで録音する手法だ。それで収録すれば、声優の立ち位置の変化などがちゃんと結果に反映される。

  簡単なプログラムでは耳や頭の形状までシミュレートすることは難しいが、音量と到達時刻だけなら三角関数で算出可能だ。それなら少なくとも「左右」については知覚可能になるのではないか。ちょっとコードを書いてみるか。

  ここはぜひオブジェクト指向で書くべきだろう。Manクラスで人の位置と向き、つまりはふたつの耳の位置を管理、SoundSourceクラスで音源の位置と波形、つまりは時間に対する音量変化を管理する。

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空間でキャラクタを動かすものが多いから、敵の場所を察知させるためにサウンドにはコダわっていると思うのだが……はて? こういうリアルさを感じたことがないな(いや、手元のゲーム機が壊れかけてから最近のゲームをしてないけれど)。

  考えてみれば、敵から発砲された銃弾が耳元をカスめる状況を再現すると、銃弾は音速を超えているから、着弾してしばらくしてから「ヒュン」なんて音が鳴るわけで、それはリアルではあるがゲーム的にはダメなのかもしれない。リアルを優先すれば、シューティングゲームなんかの多くは舞台が宇宙なわけで、グラディウスもダライアスもスターフォースも全部「無音」がリアルってことになってしまうしね。ファンタジーゾーンは……あれはファンタジーだからヨシとするとしても。

  とはいえ、レースゲームなんかにはゼヒ適用するべきじゃないかと思う。他車のエンジン音のドップラー効果は、それらしい音を作ってくっつけるより、このアルゴリズムを実装するほうがラクなくらいではないか。

  今回書いたコードはプリレンダリング(?)を前提に書いたので、ゲームには使えないが、リアルタイムにレンダリングするコードもさほど難しくはないと思う。試作してみるかなぁ。

  今回作った物件を置いておく。使うには別途cccdctに添付のlibwavが必要。