chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

カラスの鳴き声をシンセで合成したい(WAV切り取りと音声データダンプ)

背景:カラスの鳴き声をシンセで合成したい*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で計算する。。。

*1:なぜカラスの声?? 人のカーという声に似てるから・・

*2:Copilotによると、フォルマントは声道という管の共鳴周波数であり、人間の声帯を物理モデルにして表現したものであり、鳥は声帯を持たないので適用するのは誤りと言ってきた

*3:21742は、 21742(サンプル) * 2(L+R) * 2 (16bit) = 86968となり、 sub chank size: 86968 Bytes が一致する