クーの自由研究

マスターのかえるのクーは、弟子達の召喚術により新たな依り代を得てⅡ世として復活しました。

Pythonで作った音(波形)にいろいろエフェクトをかけてみよう(お題のつづき)

給料が上がる夢をみました

機材購入資金が圧倒的に不足している、かえるのクーの助手の「井戸中 聖」(いとなか せい)です。現実では給料は上がりそうにないので、一攫千金を狙います!冒険者ギルドに登録して、ダンジョン攻略にはげみます!!そしてオリハルコン・クラスを目指します!!

f:id:AssistantOfKoo:20210930001932j:plain f:id:AssistantOfKoo:20210930001744j:plain

今日のお題はフィルターとエフェクタープログラムによるリアルタイム音加工です。

前のページまでやってきたいろいろな波形に対して、フィルターやエフェクトをかけて遊びます。主にローパスフィルターと、エコー(反響/残響)処理になるかなと思います。

昨日すこしデジタルフィルタについて予習をしたので、少しだけ分かった気になってきました。いままでと同様に「まとめて処理せずに」1サンプリングづつ演算して、出力に間に合うのかを確認します。

 

エフェクト:エコー(系)

フィルターの前に簡単にできそうなエコー(反響/残響)をやってみます。

何回か前の音をてきとーに重ねればできちゃうはずです

f:id:AssistantOfKoo:20210930002215j:plain

せっかくなので、倍音成分をたくさん含んでいる「ピアノリサンプリング音」でやってみます。

実験の前に以下の微調整をします。

・現在実験用として、出力情報作成(16Bit化)まで、データ作成(create_data)部で行っているが、オシレータ部、エンベロープ部(ADSR)、出力部(float値から16Bitストリームへの変換)部を分離独立させる。出力部まですべて、内部ではfloat浮動小数点演算をする。

・あらたに作成するフィルター部、エフェクター部は、他に依存しない単独の機能とする。

・それぞれの機能をモジュール化して最終的には自由に組み換え、接続できるようにする。

当初(月曜日)の実験構想をはるかにこえる内容になってきました。

時間があるのは、今週末までなのでそれくらいに収まる内容に留めたいと思います。

 

エフェクト&フィルタを実験しやすくした基本プログラムを貼ります。

この基本プログラムをすこしずつ改変しながら実験していきます。

実験プログラムで、テストしてないまま勢いで貼っていきますので、多少のバグはご容赦ください。

~~~~~~~~~~~~~~~~~~~~~(エフェクト&フィルタ実験のための基本プログラム)

#! /usr/bin/python
# -*- coding: utf-8 -*-
import time
import pyaudio
import math
import wave
"""
Pythonで音のリアルタイム処理がどれくらい可能か実験するプログラム 2021.9.27@AssistantOfKoo
CAST IN THE NAME OF GOD, YE NOT GUILTY
"""
# Input / Output
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 48000

""" ========パラメータ操作パネル部 Parameter Settings ======== """
# *** Envelope Control ***
ATTACK = 0 #SAMPLE TIME
DECAY = 0 #SAMPLE TIME
SUSTAIN_LEVEL = 1.0 #LEVEL
SUSTAIN_TIME = 50000 #SAMPLE TIME
RELEASE = 5000 #SAMPLE TIME
# *** Filter and Effects ***
PRE_FILTER_TYPE = 0
PRE_EFFECT_TYPE = 0
POST_FILTER_TYPE = 0
POST_EFFECT_TYPE = 0

# *** Effect parameter ***
EFFECTOR1_PARAM_VOL = 1.0
EFFECTOR1_PARAM_DELAY = 24000

# *** Sampler ***
BASE_SAMPLE_FREQ = 440.0 #とりあえず A 音が基底
TARGET_SAMPLE_FREQ = [261.6256, 293.6648, 329.6276, 349.2282,
391.9954, 440.0, 493.8833, 523.2511] #ドレミファソラシト

MASTER_VOL = 0.9 # 0.01.0 :1.0より大きくすると通常破綻します。ただし演算で音が小さくなることがあるので
# その場合は1.0より大きくします。逆により小さくてもクリップする場合がありますので 1.0未満に調整が必要なことこあります。

""" -------- 汎用内部記憶装置 : GLOVAL WORK DATA -------- """
DECAY_POINT = ATTACK # ADSRの該当エンベロープが開始するサンプルポイント(わかりにくいので、名称を開始側にシフトしました)
SUSTAIN_POINT = ATTACK + DECAY
RELEASE_POINT = ATTACK + DECAY + SUSTAIN_TIME
END_POINT = ATTACK + DECAY + SUSTAIN_TIME + RELEASE

PAST_MAX = int(RATE * 1.0) # とりえず1秒ほど覚えておきます。
PAST_BUFF_L = [0.0] * PAST_MAX
PAST_BUFF_R = [0.0] * PAST_MAX

""" -------- 包絡(音の大きさ変化)発生装置 :ADSR ENVELOPE GENERATOR --------"""
def envelope(a_sample_no, chank_index):
now_sample_no = chank_index * CHUNK + a_sample_no
if now_sample_no < DECAY_POINT: # --- ATTACK ---
vol = 1.0 - (ATTACK - now_sample_no) / ATTACK
elif now_sample_no < SUSTAIN_POINT: # --- DECAY ---
vol = 1.0 - (1.0 - SUSTAIN_LEVEL) * (now_sample_no - ATTACK) / DECAY
elif now_sample_no < RELEASE_POINT: # --- SUSTAIN ---
vol = SUSTAIN_LEVEL
elif now_sample_no <= END_POINT: # --- RELEASE ---
vol = SUSTAIN_LEVEL * (END_POINT - now_sample_no) / RELEASE
else: # --- END ---
vol = 0.0
if vol < 0:
vol = 0.0 # 誤差のための安全装置
elif vol > 1.0:
vol = 1.0 # 誤差のための安全装置
return vol

""" -------- 音発生装置(サンプラー) :RESAMPLE ENGINE --------"""
def resample(a_sample_no, chank_index, a_sample_list_left, a_sample_list_right,
base_sample_freq, target_sample_freq):
target_sample_seq = 1.0 * chank_index * CHUNK + a_sample_no
ref_sample_seq = target_sample_seq * target_sample_freq / base_sample_freq
ref_sample_seq_int = int(ref_sample_seq)
ref_sample_seq_adj = ref_sample_seq - ref_sample_seq_int

sample_left_1s = a_sample_list_left[ref_sample_seq_int]
sample_left_1e = a_sample_list_left[ref_sample_seq_int + 1]
sample_left = 1.0 * sample_left_1s + 1.0 * (sample_left_1e - sample_left_1s) * ref_sample_seq_adj

sample_right_1s = a_sample_list_right[ref_sample_seq_int]
sample_right_1e = a_sample_list_right[ref_sample_seq_int + 1]
sample_right = 1.0 * sample_right_1s + 1.0 * (sample_right_1e - sample_right_1s) * ref_sample_seq_adj

return sample_left, sample_right

""" -------- 音発生装置(1サンプルフレーム) :ONE SAMPLE DATA GENERATOR --------"""
def create_one_sample(a_chunk_no, chank_index, a_sample_list_left, a_sample_list_right,
base_sample_freq, target_sample_freq):
# 音発生装置から音の1サンプルを獲得する
sample_left, sample_right = resample(a_chunk_no, chank_index, a_sample_list_left, a_sample_list_right,
base_sample_freq, target_sample_freq)

return sample_left, sample_right

""" -------- フィルター装置本体 :FILTER BODY --------"""

""" -------- フィルター装置 :FILTER SELECTOR--------"""
def sample_filter(a_filter_type, a_float_data_l, a_float_data_r):
rtn_float_data_l = a_float_data_l
rtn_float_data_r = a_float_data_r
adj_vol = 1.0
if a_filter_type == 1:
# ここを変更 Not yet implement
pass
else:
pass #BYPASS
return rtn_float_data_l / adj_vol, rtn_float_data_r / adj_vol # 音量がクリップしないように調整します

""" -------- 音響効果装置本体 :EFFECTOR BODY --------"""

""" -------- 音響効果装置 :EFFECTOR SELECTOR--------"""
def sample_effect(a_effect_type, a_float_data_l, a_float_data_r):
rtn_float_data_l = a_float_data_l
rtn_float_data_r = a_float_data_r
adj_vol = 1.0
if a_effect_type == 1:
# ここを変更 Not yet implement
pass
else:
pass #BYPASS

return rtn_float_data_l / adj_vol, rtn_float_data_r / adj_vol # 音量がクリップしないように調整します

""" -------- サンプル情報統合出荷装置 :TOTAL SAMPLE DATA CONTRACTOR --------"""
def create_data(a_chunk, a_chank_index, a_sample_list_left, a_sample_list_right, a_freq):
rtn_sample = b''
for sub_index in range(a_chunk):
# 音発生増幅装置から音を獲得
float_data_l, float_data_r = create_one_sample(sub_index, a_chank_index, a_sample_list_left, a_sample_list_right,
BASE_SAMPLE_FREQ, a_freq)
# フィルター装置で音色を変更させる(プレ処理)
float_data_l2, float_data_r2 = sample_filter(PRE_FILTER_TYPE, float_data_l, float_data_r)
# 包絡発生装置から現時点の音の大きさの指定値を獲得する
amp = envelope(sub_index, a_chank_index)
float_data_l3 = float_data_l2 * amp
float_data_r3 = float_data_r2 * amp
# 音響効果装置で音に各種の響きや変更を与える(プレ処理)
float_data_l4, float_data_r4 = sample_effect(PRE_EFFECT_TYPE, float_data_l3, float_data_r3)
# 汎用内部記憶装置に今回の音を記憶する。規定量に達していれば、古い情報を削除する
PAST_BUFF_L.append(float_data_l4)
PAST_BUFF_R.append(float_data_r4)
PAST_BUFF_L.remove(PAST_BUFF_L[0])
PAST_BUFF_R.remove(PAST_BUFF_R[0])
# フィルター装置で音色を変更させる(フィードバックさせたくない系の処理)
float_data_l5, float_data_r5 = sample_filter(POST_FILTER_TYPE, float_data_l4, float_data_r4)
# 音響効果装置で音に各種の響きや変更を与える(フィードバックさせたくない系の処理)
float_data_l6, float_data_r6 = sample_effect(POST_EFFECT_TYPE, float_data_l5, float_data_r5)
float_data_l_final = float_data_l6 * MASTER_VOL
float_data_r_final = float_data_r6 * MASTER_VOL

byte_data_l = int(float_data_l_final).to_bytes(2, 'little', signed=True)
byte_data_r = int(float_data_r_final).to_bytes(2, 'little', signed=True)

rtn_sample += (byte_data_l + byte_data_r)
return rtn_sample # サンプルがチャンク単位にまとまれば出荷する

""" -------- 発音装置 :MASTER PLAY SOUNDS --------"""
def play(a_pa, a_sample_list_left, a_sample_list_right):
stream = a_pa.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
output=True)
for note in range(8):
freq = TARGET_SAMPLE_FREQ[note]
for chank_index in range(int(END_POINT/CHUNK) + 100):
# サンプル情報統合出荷装置からの情報をうけとる
wave_data = create_data(CHUNK, chank_index, a_sample_list_left, a_sample_list_right, freq)
# pyaudioのストリームへ、うけとったチャンク単位のサンプルストリーム情報を送り出す
stream.write(wave_data)
stream.stop_stream()
stream.close()

""" -------- MAIN --------"""
if __name__ == '__main__':
pa = pyaudio.PyAudio()
sample = wave.open('StratoGen4NoiselessA.wav', 'rb') # お手持ちの音を指定する
sample_list_left, sample_list_right = [], []
for i in range(sample.getnframes()):
sound_frame = sample.readframes(1) # 2チャンネルなら左右分の情報が一度に取得される!!!
sample_list_left.append(int.from_bytes(sound_frame[0:2], 'little', signed=True))
sample_list_right.append(int.from_bytes(sound_frame[2:4], 'little', signed=True))
play(pa, sample_list_left, sample_list_right)
time.sleep(1)
pa.terminate()

~~~~~~~~~~~~~~~~~~~~~

いろいろやっているうちに、私がやってみたいエフェクトは「エコー:こだま、反響、ディレイ」ではなく、「リバーブ:残響」であることに気が付きました。

とはいえ、とりあえず「単発の反響音合成」(ディレイ)からやってみます。

エフェクト:ディレイ

好みの問題として、ピアノのディレイはあまり聞きなれていないので、ギター音に切り替えて実験してみます。せっかくなので、この前買ったピックアップでギター音を録音しました。

 

単発で遅れた音を1発だけそのまま重ねてみます。(そしてフィードバックします)

~~~~~~~~~~~~~~~~~~~~~(エフェクト実装部分のみ)

# *** Filter and Effects ***
PRE_FILTER_TYPE = 0
PRE_EFFECT_TYPE = 1
POST_FILTER_TYPE = 0
POST_EFFECT_TYPE = 0

# *** Effect parameter ***
EFFECTOR1_PARAM_VOL = 1.0
EFFECTOR1_PARAM_DELAY = int(RATE * 1.00) # max 1.0
""" -------- 音響効果装置本体 :EFFECTOR BODY --------"""
""" --- 遅延音導出装置 ---"""
def delay(a_float_data_l, a_float_data_r):
rtn_delay_sample_l = PAST_BUFF_L[PAST_MAX - EFFECTOR1_PARAM_DELAY]
rtn_delay_sample_r = PAST_BUFF_R[PAST_MAX - EFFECTOR1_PARAM_DELAY]

return EFFECTOR1_PARAM_VOL * rtn_delay_sample_l, EFFECTOR1_PARAM_VOL * rtn_delay_sample_r, EFFECTOR1_PARAM_VOL

""" -------- 音響効果装置 :EFFECTOR SELECTOR--------"""
def sample_effect(a_effect_type, a_float_data_l, a_float_data_r):
rtn_float_data_l = a_float_data_l
rtn_float_data_r = a_float_data_r
adj_vol = 1.0
if a_effect_type == 1:
# 音響効果装置からの効果音を取得します
float_delay_l, float_delay_r, effect_vol = delay(a_float_data_l, a_float_data_r)
# 音響効果装置からの音を現在の音にミキシングします
rtn_float_data_l += float_delay_l
rtn_float_data_r += float_delay_r
adj_vol += effect_vol
pass
else:
pass #BYPASS
# ここを変更 Not yet implement

return rtn_float_data_l / adj_vol, rtn_float_data_r / adj_vol # 音量がクリップしないように調整します

~~~~~~~~~~~~~~~~~~~~~

エフェクトは1発ものと、フィードバックするものを作れるようにしました。

今回はフィードバック側に差し込んでます。

f:id:AssistantOfKoo:20210930141133p:plain

処理では最大1秒前のサンプリングまで参照して演算できるようにしています。

 

実験過程で気が付いた内容として

・現実の反響音(単発)はある程度まるい音である。そのままの音をレベルを低くして遅れて重ねただけでは、非常に耳障りである。

・現実の反響音は(単発)は立ち上がりが少し抑制された感じがする。上記と同様で、すこし抑制しないと耳障りである。

ただ、これは残響(リバーブ)要素系だと思ったので、ギターのDelayとしてはこれでありとします。

 

なので、リバーブ作るときはフィルタが必須な感じなので、次はフィルタいきます。

フィルター:LPF

たぶんいっぱい公開されている&そこそこ難しいので、「ぱくる」のみです。

。。。

いろいろやりましたが、デジタルフィルターは今までの処理にくらべて処理量がとても多く、バッファ出力が間に合いません。この波形をみる限りでは「フィルタ」としてもほとんど機能していない感じがします。(それはバグですが)バグをとったとしても動きそうにありません。

f:id:AssistantOfKoo:20210930155019p:plain

音はノイズまみれなので貼りません。

そこで、ちゃんとしたデジタルフィルタの周波数特性計算に基づくものではなく、特性無視の「なんちゃって」フィルタを作ってみます。これなら少量の畳み込み積分や平均計算だけでできそうなので。。。

。。。

わ~~~。5000サンプルの平均を毎回(サンプル毎に)計算しただけでアウトです!処理が間に合いません。numpyでなら間に合いそうですが、伝家の宝刀を抜く前にlistだけでなんとかならなか悪あがきします。

どうにか、計算量を最小限におさえたフィルタをつくりましたが、「係数がすべて同じ」なおもちゃのフィルタができました。形式としてはFIRフィルタで係数はすべて「1」のフィルタです

numpyを使わずに計算量をおさえただけなので「よし」としましょう。

波形の相殺により、信号強度(振幅)が極端に弱くなるので、ソフトのマスターボリュームを上げて測定しました。

~~~~~~~~~~~~~~~~~~~~~(フィルタ実装部分のみ)

# *** Filter and Effects ***
PRE_FILTER_TYPE = 1
PRE_EFFECT_TYPE = 0
POST_FILTER_TYPE = 0
POST_EFFECT_TYPE = 0

# *** Filter parameter ***
CUTOFF_FREQ1_L = 1000.0
CUTOFF_FREQ1_R = 1000.0
PAST_FILTER_MAX = int(RATE * 0.05)  # とりえず0.05秒ほど覚えておきます。
PAST_FILTER_BUFF_L = [0.0] * PAST_FILTER_MAX
PAST_FILTER_BUFF_R = [0.0] * PAST_FILTER_MAX

SAMPLE_SUM_L = 0.0
SAMPLE_SUM_R = 0.0

""" -------- フィルター装置本体 :FILTER BODY --------"""
def toyLPF(a_sample, a_sample_array, a_summary, a_cutt_off):
# 疑似 LPF
count = int(RATE / a_cutt_off)
target_sample = a_sample_array[int(PAST_FILTER_MAX - count)]
new_summary = a_summary + a_sample - target_sample
ave = new_summary / count
return ave, new_summary

""" -------- フィルター装置 :FILTER SELECTER--------"""
def sample_filter(a_filter_type, a_float_data_l, a_float_data_r):
global SAMPLE_SUM_L
global SAMPLE_SUM_R
rtn_float_data_l = a_float_data_l
rtn_float_data_r = a_float_data_r
adj_vol = 1.0
if a_filter_type == 1:
rtn_float_data_l, SAMPLE_SUM_L = toyLPF(a_float_data_l, PAST_FILTER_BUFF_L, SAMPLE_SUM_L, CUTOFF_FREQ1_L)
rtn_float_data_r, SAMPLE_SUM_R = toyLPF(a_float_data_r, PAST_FILTER_BUFF_R, SAMPLE_SUM_R, CUTOFF_FREQ1_R)
else:
pass #BYPASS
return rtn_float_data_l / adj_vol, rtn_float_data_r / adj_vol # 音量がクリップしないように調整します

~~~~~~~~~~~~~~~~~~~~~

停滞ばっかりで、「ライブでやっている!」スピード感は皆無ですね。。。

とりあえず、ホワイトノイズで実特性をみてみます。

1000Hzのカットオフでこんな感じです。よくみる理論値に似ているのでデジタルフィルタとしては機能しているようです。

f:id:AssistantOfKoo:20210930192909p:plain

100Hzのカットオフでこんな感じです。(グラフ化のためにゲインをあげています)

# *** Filter parameter ***
CUTOFF_FREQ1_L = 100.0
CUTOFF_FREQ1_R = 100.0

f:id:AssistantOfKoo:20210930193539p:plain

今の段階では「櫛」部分をもっとなだらかにしようとしても厳しそうなので、よしとします。音はホワイトノイズを貼っても面白くないので、ギター音でフィルタの比較をします。

フィルタなし

 

1000Hz LPフィルタ

 

100Hz LPフィルタ

デジタルフィルタは調整もなんもなしの標準デジタルフィルタ以前の「しろもの」なのでひどい性能ですし、音もいわゆるデジタル臭がひどいものです。(アナログな「ボケ」味ではなく、ドット絵のような感じです)

なお、100Hzフィルタの「ソ(G):391.9954」の音がほとんどでていませんが、これはデジタルフィルタの400Hzの「櫛」にひっかかったためで、理論通りの現象です。

f:id:AssistantOfKoo:20210930204106j:plain

調子がよければHPFやBPFも作成するつもりでしたが、この有様なので、実験としてはこの辺にしようと思います。

リアルタイム(1サンプル毎)のデジタルフィルタの実装は困難である!(numpyとかnumbaをつかわないPythonで、です)

1チャンク単位(1024サンプルetc)でnumpyを使って計算すれば、おそらくはIIRでも「いける」気がします。

 

ほかのエフェクターやフィルターもどんどん作り逃げするつもりでしたが、デジタルフィルタで「はまって」しまいましたので、今回はこの辺でお開きにしようかと思います。

 

この週末までは時間があるので、気が向けばもう少し悪あがきするかもしれません。

ご清聴どうも有難うございました!

悪あがき

いや、フィルタを使わない系のエフェクターならもう少しいけるんでない?

エフェクト:コンプレッサー

波形は意図した変形をおこなえましたが、コンプレッサーではなく、オーバードライブ+ディストーション系になりました。まぁ、そうでしょう。

波形を -4.0db (int32の整数値で 16384)を基準として、自然対数(e)を底にして対数圧縮しています。compress値がゼロで元の波形と同じです。

~~~~~~~~~~~~~~~~~~~~~(変更部分のみ)

INT_MAX = 32768
# *** Effect parameter ***
(略)
EFFECTOR2_PARAM_COMP_L = 100.0
EFFECTOR2_PARAM_COMP_R = 0.0
def compress(a_sample, a_compress):
""" --- 対数圧縮装置 --"""
""" --- -4.0dBを基準として、自然対数(e)圧縮を行う compress = 0.0 to 100.0 ---
compress = 0.0 で圧縮なし
compress = 100.0 で最小信号に対して 40.0dB(100)のゲインとなるようにパラメータ設定しています
compress変化 に対して ゲイン(dB)Bカーブ的になります  """
if a_sample == 0:
rtn_compress = 0.0
else:
sign = 1.0 if a_sample >= 0 else -1.0
val = a_sample * sign
base_val = (1 + math.log10((1 + a_compress * 0.07)))
rtn_compress = math.exp(math.log(val * 2) / base_val) * INT_MAX / (2 * math.exp(math.log(INT_MAX) / base_val)) * sign
return rtn_compress
""" -------- 音響効果装置 :EFFECTOR SELECTER--------"""
def sample_effect(a_effect_type, a_float_data_l, a_float_data_r):
(略)
pass
elif a_effect_type == 2:
rtn_float_data_l = compress(a_float_data_l, EFFECTOR2_PARAM_COMP_L)
rtn_float_data_r = compress(a_float_data_r, EFFECTOR2_PARAM_COMP_R)
else:
pass #BYPASS
# ここを変更 Not yet implement

return rtn_float_data_l / adj_vol, rtn_float_data_r / adj_vol # 音量がクリップしないように調整します

~~~~~~~~~~~~~~~~~~~~~

比較のため、左側(ツールだと上側)だけエフェクトしています。

f:id:AssistantOfKoo:20211001020205p:plain

波形的には意図とおりの変形ができています。

音はこんな感じです。デジタル風味満載です。

興味本位で-4.0dB基準(-4.0dBを超える信号は逆に小さくなる)にしましたが、 0.0dB基準でよかった気がします。まぁ、実験なので、よしとします。

こんな音をそのまま使うことはないのですが、フィルタでもう少し音を「丸く」して使うとい、いい感じかもしれません。

 

まとめ

「リアルタイムでできる」ので、バッファリングやその他の本当にたくさんの事前・事後処理が省略できる(そもそも不要になる)ので、コアな部分の実験が本当に簡単にできました。これなら、他のいろいろな音響・音処理実験も、とっても簡単にできる感じがします。

今回はいい感じで収穫がありました。

もしかしたら、昔苦労してもできなかったことが、「いまどき」のPCスペックであれば簡単にできてしまうことがあるのかもしれません。

あまり過去経験をもとにした先入観を持たないようにして、実験をしてききます。

時はきた!

f:id:AssistantOfKoo:20211001023508p:plain

2期はよ!

www.youtube.com