背景:カラスの鳴き声をシンセで合成したい*1
取り組み:まずは鳴いているカラスをPCMレコーダで録音、鳴いてる時間帯だけ切り出し
詳細:
OM SystemのPCMレコーダで鳴いてるカラスを録音した。それをPCに持ってきて、音声編集ソフトのAudaciutyで鳴き声の時間帯を切り取り

サンプリングレート:48000Hz、エンコーディング:Signed 16bit、ステレオでWAV形式で出力

出力されたデータはWAV形式なのでPython等でデータ処理できるはず
スマフォにFFTアプリ(FFT Wave)を入れていて、それで計測すると、鳴き声は1400Hzあたりにピークが来ていた。耳で聞く限り、鳴き始めてから高さが下がる印象なので、詳細に調べると、周波数のピークは1400から時間と共に下がっていくと思われる。時間経過でどの周波数帯が強いのか?を表示して声紋というのか、波紋というのか、そういうのを詳しく観察できるアプリを作るつもり。(まずはPython+GUIモジュールで試作して、高速化のためC#で作る予定)。例によって、DFTの辺りがめちゃええ加減なのですが、、sin/cosで離散的に掛け算して(周波数ごと*時間ごと)の強さを出す予定。多分フォルマント*2とかいう声のモデルで分析できて、合成技術を転用したら似た鳴き声ができるのではないかと。。。
勉強を兼ねて、音声処理用のツールは使わず、全て自分で作る予定。まずはお約束のWAV形式のヘッダ部の解析から。。 仕様書の説明はYoufit様の解説記事を参照
音ファイル(拡張子:WAVファイル)のデータ構造について - 福岡・東京のシステム開発会社 (株)ユーフィット
多分ソースはあちこちに出てると思うが、、生成AIがソースを書いてくれる時代に無駄かもしれないが、、勉強を兼ねてWAVヘッダを読むソースを作った。
#!/usr/bin/python3
import struct
FILE='260227_0038.wav'
def read_WAV_headers(f):
# read RIFF Identifier
header_raw_4b = f.read(4)
riff_idt = header_raw_4b.decode('ascii')
print('RIFF Identifyier:', riff_idt)
# read chank size
header_raw_4b = f.read(4)
#print(header_raw_4b)
chank_size = struct.unpack("<I", header_raw_4b)[0]
print('chank size:', chank_size, 'Bytes')
# read format
header_raw_4b = f.read(4)
file_format = header_raw_4b.decode('ascii')
print('file format:', file_format)
# fmt identifier
header_raw_4b = f.read(4)
fmt_idt = header_raw_4b.decode('ascii')
print('fmt identifier:', fmt_idt)
# bytes of fmt chank
header_raw_4b = f.read(4)
byte_size_of_chank = struct.unpack("<I", header_raw_4b)[0]
print('byte_size_of_chank:', byte_size_of_chank)
# format
header_raw_2b = f.read(2)
voice_format = struct.unpack("<H", header_raw_2b)[0]
print('voice_format:', voice_format)
# n of channels
header_raw_2b = f.read(2)
n_of_channels = struct.unpack("<H", header_raw_2b)[0]
print('n_of_channels:', n_of_channels, 'ch')
# sampling freq
header_raw_4b = f.read(4)
sampling_ferq = struct.unpack("<I", header_raw_4b)[0]
print('sampling_ferq:', sampling_ferq, 'Hz')
# Byte per sec
header_raw_4b = f.read(4)
voice_bps = struct.unpack("<I", header_raw_4b)[0]
print('voice_Bps:', voice_bps, 'Bps')
# block size
header_raw_2b = f.read(2)
block_size = struct.unpack("<H", header_raw_2b)[0]
print('block_size:', block_size)
# bits per sample
header_raw_2b = f.read(2)
bits_per_sample = struct.unpack("<H", header_raw_2b)[0]
print('bits_per_sample:', bits_per_sample)
# skip extention params(2B)
# skip extention params(NB)
# sub chank identifier
header_raw_4b = f.read(4)
chank_idt = header_raw_4b.decode('ascii')
print('sub chank identifier:', chank_idt)
# size of sub chank
header_raw_4b = f.read(4)
print('chank identifier:', chank_idt)
size_of_sub_chank = struct.unpack("<I", header_raw_4b)[0]
print('sub chank size:', size_of_sub_chank, 'Bytes')
with open(FILE,'rb') as f:
read_WAV_headers(f) 実行した結果は以下
$ ./test.py RIFF Identifyier: RIFF chank size: 87004 Bytes file format: WAVE fmt identifier: fmt byte_size_of_chank: 16 voice_format: 1 n_of_channels: 2 ch sampling_ferq: 48000 Hz voice_Bps: 192000 Bps block_size: 4 bits_per_sample: 16 sub chank identifier: data chank identifier: data sub chank size: 86968 Bytes
多分ヘッダはOKということで、、音声データを読み込んでみる。LLRRLLRRのように片側2ByteでLittleEndianで構成されているので、、データ読み込みコードは以下となる。 (< は little endian, h は 符号付き2byte指定(signed short))
sample_raw_4b = f.read(4)
sample_l, sample_r = struct.unpack("<hh", sample_raw_4b)CSV形式でLRを出力コードは以下 (PythonでPIL?使ってグラフ書くべきかもですが、、まずは地道に動作確認から)
#!/usr/bin/python3
import struct
IN_FILE='260227_0038.wav'
OUT_FILE='sound.csv'
def read_WAV_headers(f):
# read RIFF Identifier
header_raw_4b = f.read(4)
riff_idt = header_raw_4b.decode('ascii')
print('RIFF Identifyier:', riff_idt)
# read chank size
header_raw_4b = f.read(4)
#print(header_raw_4b)
chank_size = struct.unpack("<I", header_raw_4b)[0]
print('chank size:', chank_size, 'Bytes')
# read format
header_raw_4b = f.read(4)
file_format = header_raw_4b.decode('ascii')
print('file format:', file_format)
# fmt identifier
header_raw_4b = f.read(4)
fmt_idt = header_raw_4b.decode('ascii')
print('fmt identifier:', fmt_idt)
# bytes of fmt chank
header_raw_4b = f.read(4)
byte_size_of_chank = struct.unpack("<I", header_raw_4b)[0]
print('byte_size_of_chank:', byte_size_of_chank)
# format
header_raw_2b = f.read(2)
voice_format = struct.unpack("<H", header_raw_2b)[0]
print('voice_format:', voice_format)
# n of channels
header_raw_2b = f.read(2)
n_of_channels = struct.unpack("<H", header_raw_2b)[0]
print('n_of_channels:', n_of_channels, 'ch')
# sampling freq
header_raw_4b = f.read(4)
sampling_ferq = struct.unpack("<I", header_raw_4b)[0]
print('sampling_ferq:', sampling_ferq, 'Hz')
# Byte per sec
header_raw_4b = f.read(4)
voice_bps = struct.unpack("<I", header_raw_4b)[0]
print('voice_Bps:', voice_bps, 'Bps')
# block size
header_raw_2b = f.read(2)
block_size = struct.unpack("<H", header_raw_2b)[0]
print('block_size:', block_size)
# bits per sample
header_raw_2b = f.read(2)
bits_per_sample = struct.unpack("<H", header_raw_2b)[0]
print('bits_per_sample:', bits_per_sample)
# skip extention params(2B)
# skip extention params(NB)
# sub chank identifier
header_raw_4b = f.read(4)
chank_idt = header_raw_4b.decode('ascii')
print('sub chank identifier:', chank_idt)
# size of sub chank
header_raw_4b = f.read(4)
print('chank identifier:', chank_idt)
size_of_sub_chank = struct.unpack("<I", header_raw_4b)[0]
print('sub chank size:', size_of_sub_chank, 'Bytes')
return size_of_sub_chank
def read_sample_flame(f):
# read 1 sample flame
sample_raw_4b = f.read(4)
sample_l, sample_r = struct.unpack("<hh", sample_raw_4b)
return(sample_l, sample_r)
with open(IN_FILE,'rb') as fp_r, open(OUT_FILE,'w') as fp_w:
size_of_sub_chank = read_WAV_headers(fp_r)
for _ in range(int(size_of_sub_chank/4)):
(sample_l, sample_r) = read_sample_flame(fp_r)
#print(sample_l, sample_r)
fp_w.write(f'{sample_l},{sample_r}\n')ExcelでCSVを読み込ませてLchだけグラフ化したら以下

(N=21742) *3
Audactyの切り抜きデータの波形に近いので、まぁ読み込めたと判断
で、、次はこのデータにDFTをかけると、どの周波数が強いかを取り出せると。。DFTはPythonライブラリあると思いますが、、ここはナンチテDFTで、sin/cosで計算する。。。