広告/統計/アニメ/映画 等に関するブログ

広告/統計/アニメ/映画 等に関するブログ

Pythonで救急車のサイレンの音を作り、Blenderでドップラー効果をかける

救急車のサイレンを作ってドップラー効果をかける必要があったので備忘録。Pythonでモノラルのサイン派を作り、Blenderドップラー効果をかけるのが最も楽だろうと判断した

Pythonで救急車のサイレンを作る

救急車のサイレン

救急車のサイレンの音は960 Hz と 770 Hzが0.6秒毎に切り替わっているらしい(実を言うと他では1.3秒という記載もあり、ちゃんと元の法令を調べていないので、正確なところは保証しない)。

救急車の位置情報をサイレン音に載せて周囲のカーナビに表示 | NICT-情報通信研究機構

音楽ソフトではこのような周波数を決め打ちで音を作るのは難しいので、Pythonで元の音を作るのが手軽である。

ライブラリのインポート

numpyとwaveだけでよい。

import numpy as np
import matplotlib.pyplot as plt #グラフ表示用
import wave

数値の指定

再生する時間、サンプリングレート、作りたいサイン派の周波数、切り替わり秒数、などを変数に入れておく

Amp = 1 #振幅。-1~1で正規化したデータを作るので1にしておく。
fs = 44100 #サンプリングレート(Sampling Frequency)
f1 = 960 #サイレンの周波数1(単位:Hz)
f2 = 770 #サイレンの周波数2(単位:Hz)
sec = 1*60 #再生させる時間(単位:秒)
nframe = np.arange(0,fs*sec) #フレーム数=サンプリングレート*秒数
nframe_1term = np.arange(0,int(fs*0.6)) #サイレンの周波数は0.6秒毎に周波数が切り替わる。その1回分

それぞれの周波数1回分のサイン波を作る

python上での時刻というかx軸にあたるものは秒単位ではなくサンプリングレートの1フレームが単位になる。すっかり学生時代の記憶が薄いのでこちらのサイトを見て思い出した。

izumi-math.jp

sin(「角速度(2pi*周波数Hz)×秒数(=フレーム数/サンプリングレート)」)ということになるので、それぞれの周波数のサイン派は以下のようになる。

sin_wave_1_1term = Amp*np.sin((2*np.pi*f1)*(nframe_1term/fs))
sin_wave_2_1term = Amp*np.sin((2*np.pi*f2)*(nframe_1term/fs))

冒頭をグラフにすると以下のようにサイン派になっていることがわかる。(0.6秒だと0で終わらなくない?というのが気になるが、目をつむる。)

fig,ax = plt.subplots(2,1,figsize=(16,9),dpi=500)
ax[0].plot(sin_wave_1_1term[0:120],label="960Hz")
ax[0].plot(np.zeros(120))
ax[1].plot(sin_wave_2_1term[0:120],label="770Hz")
ax[1].plot(np.zeros(120))
ax[0].legend(loc="upper left")
ax[1].legend(loc="upper left")
plt.savefig("sin_wave_1term.svg")
plt.savefig("sin_wave_1term.pdf")
plt.close()

2つのサイン波

2つのサイン派を連続させて1セット分を作る

sin_wave_1term = np.concatenate([sin_wave_1_1term,sin_wave_2_1term],axis=0)

再生させたい秒数分このセットを繰り返す

ここでもフレーム数があくまで時間の単位になる

sin_wave = np.tile(sin_wave_1term,int(fs*sec/len(sin_wave_1term)))

Waveファイルにするにあたって整数にする。(int16を採用)

WAVEファイルはバイナリーなデータが書き込まれるのでsin派の小数点以下の数値は全て整数にしなければならない。pythonの整数型には精度が異なるものがあるのでどれを採用するかで何桁まで再現できるかが変わる。

qiita.com

qiita.com

sin_wave_frames = np.array(sin_wave*(2**15-1)).astype(np.int16)

Waveファイルに書き出す

どのライブラリを使うかは好みがわかれる。今回はこの時点ではモノラル音源で問題ないので標準ライブラリで完結してしまうのは面倒ではない。

Pythonでのwavファイル操作 - Qiita

wavの仕組みと読み込み・書き出し方法【pythonで音響信号処理】 | もろみ先輩の日常

量子化ビット数はだいたい16bitなので16bitにしておく

output_file = "original_siren.wav"
file = wave.open(output_file,"wb")
file.setnchannels(1) # モノラルで良い
file.setsampwidth(2) # 量子化ビット数 16bit/8=2
file.setframerate(fs)
file.writeframes(sin_wave_frames)
file.close()

Blenderドップラー効果をかける

Blenderにはスピーカーを配置することができる。更にそのスピーカーに音源データを配置できるので、スピーカーを時刻ごとに異なる位置に存在するようにキーフレームを打っておけば、自動的にドップラー効果が反映されたステレオ音源にすることができる。

スピーカーに配置する音源データは後で長さを変えることができないので、元の音源の長さは余裕をもった長さにしておく方が賢明。

スピーカーの配置

カメラなどと同様のところからスピーカーが設置できる

スピーカーの挿入

音符マークのプロパティからファイルを挿入できる

スピーカーの音源配置

スピーカーを何軸上を動かすのか?自分(=カメラ)との距離は?などを考えてカメラの位置や向きを変える。なおスピーカーと近すぎるとちゃんとノイズが入るので、ちょっと遠くしておくのが良さげ。

カメラの配置

スピーカーの移動

1フレーム時点で、x軸上で「-15m」の位置とした。どれくらいの時速にするかは決めの問題。

スピーカー_スタート地点

動画のフレームレートを30fpsにしたので、3秒=91フレ時点でx軸上「0m」の位置にあるようにした。自分がわかりすいようにここにキーフレームを打っているが、実際には動画の頭と最後に打っておけば良い。

スピーカー_90フレ

動画の最後1801フレで135mとした。

スピーカー1800フレ

音の書き出し

レンダリング設定でステレオにできる。音だけで良いのでno videoにしておく。

音の書き出し

音だけをレンダリングできる

レンダリング

参考動画

以上の工程で作った「ドップラー効果なし」「ドップラー効果あり」の音を並べた動画。

Blenderドップラー効果をかけた時点で一度カメラを通した音の記録になっているので、音量バランスが元の素材より概ね小さくなっているので、2つ並べるときは音量の調整が必要。

youtu.be

Pythonコード全体

# -*- coding:utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
import wave

# 数値の指定
Amp = 1 #振幅。-1~1で正規化したデータを作るので1にしておく。
fs = 44100 #サンプリングレート(Sampling Frequency)
f1 = 960 #サイレンの周波数1(単位:Hz)
f2 = 770 #サイレンの周波数2(単位:Hz)
sec = 1*60 #再生させる時間(単位:秒)
nframe = np.arange(0,fs*sec) #フレーム数=サンプリングレート*秒数
nframe_1term = np.arange(0,int(fs*0.6)) #サイレンの周波数は0.6秒毎に周波数が切り替わる。その1回分

# それぞれの周波数1回分のサイン波を作る
# 角速度(2pi*周波数Hz)×秒数(=フレーム数/サンプリングレート)
# http://izumi-math.jp/M_Sanae/Fourier/four_1_2.htm
sin_wave_1_1term = Amp*np.sin((2*np.pi*f1)*(nframe_1term/fs))
sin_wave_2_1term = Amp*np.sin((2*np.pi*f2)*(nframe_1term/fs))

# グラフで確認
fig,ax = plt.subplots(2,1,figsize=(16,9),dpi=500)
ax[0].plot(sin_wave_1_1term[0:120],label="960Hz")
ax[0].plot(np.zeros(120))
ax[1].plot(sin_wave_2_1term[0:120],label="770Hz")
ax[1].plot(np.zeros(120))
ax[0].legend(loc="upper left")
ax[1].legend(loc="upper left")
plt.savefig("sin_wave_1term.svg")
plt.savefig("sin_wave_1term.pdf")
plt.close()

# 2つのサイン派を連続させて1セット分を作る
sin_wave_1term = np.concatenate([sin_wave_1_1term,sin_wave_2_1term],axis=0)
# 再生させたい秒数分このセットを繰り返す
sin_wave = np.tile(sin_wave_1term,int(fs*sec/len(sin_wave_1term)))

# Waveファイルにするにあたって整数にする。(int16を採用)
# https://qiita.com/Oka_D/items/86db73ab54dd7b4bc72b
# https://qiita.com/Oka_D/items/34e9f6c47962f51946c1
sin_wave_frames = np.array(sin_wave*(2**15-1)).astype(np.int16)

# Waveファイルに書き出す
# https://moromisenpy.com/python_wav/
# https://qiita.com/Dsuke-K/items/2ad4945a81644db1e9ff
# https://docs.python.org/ja/3/library/wave.html
output_file = "original_siren.wav"
file = wave.open(output_file,"wb")
file.setnchannels(1) # モノラルで良い
file.setsampwidth(2) # 量子化ビット数 16bit/8=2
file.setframerate(fs)
file.writeframes(sin_wave_frames)
file.close()