ADX2ピーク部分にマーカーを作成するスクリプト

#はじめに
Pythonでの音の解析の勉強もかねて
音を解析して簡易なピーク検出をして、マーカーを配置するものを作ってみた。

環境: CRI AtomCraft 3.45.00

#動作
キューを選択してスクリプトを実行するとマーカーが作られます。

https://twitter.com/tatmos/status/1416493475302809600

#音の解析について
あらかじめ断っておきますが、雑な処理かと思います。
・波形をpyaudioを利用してバッファに取り込み
・numpyで処理フレームごとにFFTして低い方のレベルをチェックし
・scipyでピークを抽出しています。

弱いピークも検出してしまったり、近接にアタックがあると取り逃したりするかもしれないです。
maxid = sp.argrelmax(amp_all, order=100)
のorderを変更すると検出範囲が変わります。

N = 64 #解析するデータ数　（2のべき乗が良い）
BIN = 1#調査するBIN　（FFT結果のどこを見るか）

FFTなので、分析時間精度と周波数のどこを見るかのトレードオフがあります。

#スクリプト

最初の方は、ADX2のマテリアルで言語が異なる時などの対応コードがいろいろ書いてありますが、
言語を利用していなければ特に問題ないはず。

``` WavToPeakMk.py
# --Description:[tatmos][Preview]選択したオブジェクトの原音のレベルをチェックしマーカーデータ作成
import pyaudio
import wave
import time
import numpy as np
import scipy.signal as sp
import cri.atomcraft.project as acproject
import cri.atomcraft.debug as acdebug
from struct import unpack

#ndarrayの内容出力を省略しない
#np.set_printoptions(threshold=np.inf)

p = pyaudio.PyAudio()

#カレント言語
user_settings = acproject.get_user_settings()["data"]
language_setting = acproject.get_value(user_settings, "CurrentLanguageSetting")["data"]
language = acproject.get_value(language_setting, "OutputSuffix")["data"]
print(language)
languageFolderJa = "/ja/"
languageFolderEn = "/en/"

#言語マテリアルを得る
def getLanguageMaterial(tmplinkMaterialPath):
    #acdebug.log("Target Path:\"{0}\"".format(tmplinkMaterialPath ))
    linkMaterialPath = tmplinkMaterialPath

    #言語フォルダーのマテリアルパスに変更
    if language == "_ja":
        linkMaterialPath = tmplinkMaterialPath.replace(languageFolderEn,languageFolderJa)
    else:
        linkMaterialPath = tmplinkMaterialPath.replace(languageFolderJa,languageFolderEn)

    #言語フォルダーのマテリアルに変更
    resultMaterial = acproject.get_object_from_path(linkMaterialPath)
    #acdebug.log("resultMaterial :\"{0}\"".format(resultMaterial["succeed"] ))
    #acdebug.log("resultMaterial :\"{0}\"".format(resultMaterial["data"] ))
    if resultMaterial["data"]:
        #acdebug.log("resultMaterial :\"{0}\"".format(linkMaterialPath ))
        linkMaterial = resultMaterial["data"]
    else:
        #acdebug.warning("Not Found Target Path:\"{0}\"".format(linkMaterialPath ))
        linkMaterial = acproject.get_object_from_path(tmplinkMaterialPath)["data"] #みつからない時
    return linkMaterial


#--------

#選択中のウェーブフォームリージョンまたはキューを得る
selectedWaveformRegions = acproject.get_selected_objects("WaveformRegion")["data"]
selectedCues = acproject.get_selected_objects("Cue")["data"]

selectedMaterial = None
selectedCue = None

if len(selectedCues) > 0:
    selectedWaveformRegions = acproject.find_objects(selectedCues[0],"WaveformRegion")["data"]
    selectedCue = selectedCues[0]
else:
    selectedCue = acproject.get_parent(selectedWaveformRegions[0],"Cue")["data"]

if len(selectedWaveformRegions) > 0:
    selectedMaterial = acproject.get_value(selectedWaveformRegions[0], "LinkMaterial")["data"]

#言語違いマテリアルを得る
selectedMaterial = getLanguageMaterial(acproject.get_object_path(selectedMaterial)["data"] )

if not selectedMaterial:
    acdebug.warning("解析するウェーブフォームリージョンまたはキューを選択してください。")
    sys.exit()

print(acproject.get_value(selectedMaterial, "Name")["data"])

input_path = acproject.get_value(selectedMaterial, "SrcFileAbsolutePath")["data"]

if not input_path:
    acdebug.warning("マテリアルのパスがみつかりません。")
    sys.exit()

print("Path:" + str(input_path))

#--------
#ファイルを開く
wf = wave.open(input_path, "rb")

buf = wf.readframes(-1)    #全部bufferに読む

wf.close()

#データ解析
if wf.getsampwidth() == 2: #16bit波形
    ndarray = np.frombuffer(buf, dtype='int16')    #2byteごとに配列化 (-32768 - 32767)
elif  wf.getsampwidth() == 3:#24bit波形
    read_frames = wf.getnframes()
    nbyte = wf.getsampwidth()
    data = [unpack("<i",
            bytearray([0]) + buf[nbyte * idx: nbyte * (idx + 1)])[0]
            for idx in range(read_frames)]
    ndarray = np.array(data, dtype='int16')
elif samplewidth == 4:# 32bit (未確認)
    ndarray = np.frombuffer(buf, dtype='int32')


#ステレオ時は左のみ
if wf.getnchannels() == 2:
    ndarray = ndarray[0::2]

N = 64 #解析するデータ数

BIN = 1#調査するBIN



#ndarrayの長さを得る
print(len(ndarray))
#あまり分を足す
padding = N - len(ndarray) % N
print(padding)
ndarray = np.append(ndarray,np.zeros(padding))
#print(len(ndarray))
#print(len(ndarray) / N)
#均等に分割
ndarray = np.split(ndarray, len(ndarray) / N)

#処理単位が何msecか
msec_per_frame = N / wf.getframerate() * 1000
print("msec_per_frame " + str(msec_per_frame))

#all = ""
amp_all = []

currentMsec = 0
for frame in ndarray:   #処理フレームごとにFFT
    # FFT
    wave_y = np.fft.fft(frame)
    wave_y = np.abs(wave_y) / N * 2 #周期を得るために絶対値 データ数で割って2倍にして正規化
    amp_all.append(wave_y[:int(N / 2) + 1][BIN]) # 特定のバンドのみ
    currentMsec += msec_per_frame

# ピークのみ抽出        
amp_all = np.array(amp_all)
maxid = sp.argrelmax(amp_all, order=100) #最大値

#---------
# マーカーの作成
for id in maxid[0]:
    new_marker = acproject.create_object(selectedCue,"CallbackMarker","mk" + str(id))["data"]
    acproject.set_value(new_marker, "MarkerStartTime", id*msec_per_frame)	
    acproject.set_value(new_marker, "CallbackTag", str(amp_all[id]))	

#---------
#Debug
#for id in maxid[0]:
#    all += "{} : {}\n".format(id*msec_per_frame ,str(amp_all[id]))


##ファイルに解析内容を保存
#sample_file = open("C:\MyDearest\peak.txt", mode="w")
#try:
#    sample_file.write(str(all))
#finally:
#    sample_file.close()

p.terminate()
```

#おわりに
音の解析からデータを作成できるといろいろ都合が良さそうな場面があるかと思います。
音に合わせてコールバックとか飛ばせたら、ゲームの演出などに使えるかもしれません。

FFTなら窓をかけたり、オーバーラップとかすると結果が変わるかと思いますが、
とりあえず、らしいものができた。
（今回の精度としてはこれくらいでも良いかも）

あと、Pythonには音の解析系のライブラリもいろいろあるので組み合わせたらいろいろできそうな気がしています。