クーの自由研究

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

Pythonでいろんな波形をつくってみよう(お題のつづき)

命にかかわらない荒波は人を成長させるとつくづつ感じます

小林さんのように目が死んでいる、かえるのクーの助手の「井戸中 聖」(いとなか せい)です。

にわかに、ややワイルド感のある「ハーフアップポニーテール」が流行っているようなのでチャレンジしてみます。マスクしていても目に覇気があると、さらに存在感が増します。

f:id:AssistantOfKoo:20210929091817p:plain

f:id:AssistantOfKoo:20210929092110j:plain

(c)MAQUIA ONLINE 様

さて、1ページぶりのご無沙汰でした。ひきつづきPythonで「リアルタイム」波形作成をしていきます。

矩形波のPWM(Pulse Width Modulation)

PWMはPCファンの回転制御にも使用されています

f:id:AssistantOfKoo:20210929094508p:plain

パルス幅を変えることにより、音響的には倍音構成がいい感じに変わっていきます。

Dutyが50%以外では音響的なDCオフセット(波形の平均値が直流成分を持つ)が発生します。

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

FRQ_L = 440.0
FRQ_R = 442.0
DUTY = 30 #1 to 99
PWM_THRESHOLD = 1.0 - DUTY / 50.0
MASTER_VOL = 0.3
#PWM Wave
base_val_l = FRQ_L * 2.0 * (a_index * CHUNK + a_sample_no) / RATE
saw_val_l = base_val_l - 2.0 * int(base_val_l / 2) - 1.0
pwm_sample_l = (1.0 if (saw_val_l > PWM_THRESHOLD) else -1.0)
float_data_l = pwm_sample_l * MAX_SIG * vol * MASTER_VOL
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)

base_val_r = FRQ_R * 2.0 * (a_index * CHUNK + a_sample_no) / RATE
saw_val_r = base_val_r - 2.0 * int(base_val_r / 2) - 1.0
pwm_sample_r = (1.0 if (saw_val_r > PWM_THRESHOLD) else -1.0)
float_data_r = pwm_sample_r * MAX_SIG * vol * MASTER_VOL
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_data_l + byte_data_r

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

さらに凶悪な音のため、マスターボリューム値をつけて音量を絞りました。

ノコギリ波に閾値を適用することで実現しています。ご覧の通りDCオフセット補正は行っていません。

f:id:AssistantOfKoo:20210929102533p:plain

では、次にDUTYを動かしてみます

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

FRQ_L = 440.0
FRQ_R = 442.0
DUTY_MIN = 20 #1 to 99
DUTY_MAX = 80 #1 to 99
PWM_FREQ = 4
MASTER_VOL = 0.4
#PWM Wave
duty = (DUTY_MIN + DUTY_MAX) / 2 + (DUTY_MAX - DUTY_MIN) / 2 *\
math.sin(PWM_FREQ * 2 * math.pi * (a_index * CHUNK + a_sample_no) / RATE)
pwm_threshokd = 1.0 - duty / 50.0
base_val_l = FRQ_L * 2.0 * (a_index * CHUNK + a_sample_no) / RATE
saw_val_l = base_val_l - 2.0 * int(base_val_l / 2) - 1.0
pwm_sample_l = (1.0 if (saw_val_l > pwm_threshokd) else -1.0)
float_data_l = pwm_sample_l * MAX_SIG * vol * MASTER_VOL
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)

base_val_r = FRQ_R * 2.0 * (a_index * CHUNK + a_sample_no) / RATE
saw_val_r = base_val_r - 2.0 * int(base_val_r / 2) - 1.0
pwm_sample_r = (1.0 if (saw_val_r > pwm_threshokd) else -1.0)
float_data_r = pwm_sample_r * MAX_SIG * vol * MASTER_VOL
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_data_l + byte_data_r

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

f:id:AssistantOfKoo:20210929104832p:plain

実験例では20%から80%のデューティ比までを4Hzのサイン波で動かしています。パルス幅が時間とともにかわっているのがわかります。

 

矩形波PWMも問題なくリアルタイムでできました!

では次いきます

周波数変調方式(Frequency Modulation)

現在しくみを勉強中。。。

なるほど、位相部分にむりやり特定時刻の波の値をつっこんじゃえばよいのですね!

さすがに学習しながらパラメータ設定は難しいので、オーソドックスにいきます。

それにしても時間に関する関数の設定が多すぎます。今回は大幅に端折って「なんちゃって」でいきます。

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

FRQ_L = 440.0
FRQ_R = 442.0
PARAM_A = 3.8
PARAM_B = 4.2
PARAM_C = 0.7
PARAM_D = 5.0
MASTER_VOL = 0.4
#Frequency Modulation Wave
sample_seq = a_index * CHUNK + a_sample_no
fase_value = 2 * math.pi * sample_seq / RATE
Base_l = math.sin(FRQ_L * fase_value)
mod1_l = math.sin(PARAM_A * Base_l + PARAM_C * math.sin(PARAM_B * fase_value))
mod2_l = math.sin(PARAM_D * mod1_l)
float_data_l = mod2_l * MAX_SIG * vol * MASTER_VOL
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)

Base_r = math.sin(FRQ_R * fase_value)
mod1_r = math.sin(PARAM_A * Base_r + PARAM_C + math.sin(PARAM_B * fase_value))
mod2_r = math.sin(PARAM_D * mod1_r)
float_data_r = mod2_r * MAX_SIG * vol * MASTER_VOL
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)

return byte_data_l + byte_data_r

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

プログラムでは2次変調までさせてみました。

変調パラメータや変調代入の組み合わせによりいろいろ音がかわります。

やはり目的の音色に近づけるのは難しいですね。

波形グラフを見ると、いかにもFM変調っぽい波形になっています。

f:id:AssistantOfKoo:20210929120934p:plain

 

FM音源も問題なくリアルタイムでできました!

では次いきます

番外編:サンプラー方式

サンプラーは基本的は録音(実波形記憶など)をもとに、異なる周波数でその音を再現させるものです。基本波形と再生周波数が離れていると不自然な音になりますが、近いとそこそこいい音がします。

f:id:AssistantOfKoo:20210929125430p:plain

48.0KHz/32Bit(浮動小数点) サンプリングのピアノ音があったので、これを使ってみます。

単一音フルサンプリング(減衰音が切れるまで)なので27Secくらいあります。

Steinway  & Souns 社のピアノのようです。A音を選んでみます。ピッチが少し怪しい気もしますが、実験なので気にしません。

 
下の波形ファイル読み込みで、32bit浮動小数点とでていたことと

f:id:AssistantOfKoo:20210929192705p:plain f:id:AssistantOfKoo:20210929194930p:plain

framesizeが4が片チャンネルデータで4バイトであると完全に思い込んでいたため、取得データを浮動小数点として扱おうとして破綻していました。

ソフトの表記は読み込みファイルのフォーマットではなく、現在の編集モードが「それ」である意味のようです。

上記でサンプルファイルが32Bit浮動小数点と書いたのは誤りで、結局 16Bitでした。無駄に時間を使ってしまいましたが、いい勉強になりました。

~~~~~~~~~~~~~~~~~~~~~(全部貼り直します)

#! /usr/bin/python
# -*- coding: utf-8 -*-
import time
import pyaudio
import math
import wave

CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 48000
MAX_SIG = 32000
ATTACK = 0 #SAMPLE TIME
DECAY = 0 #SAMPLE TIME
SUSTAIN_LEVEL = 1.0 #LEVEL
SUSTAIN_TIME = 50000 #SAMPLE TIME
RELEASE = 5000 #SAMPLE TIME
DECAY_POINT = ATTACK + DECAY
SUSTAIN_POINT = ATTACK + DECAY + SUSTAIN_TIME
END_POINT = ATTACK + DECAY + SUSTAIN_TIME + RELEASE
BASE_SAMPLE_FREQ = 440.0
TARGET_SAMPLE_FREQ = [261.6256, 293.6648, 329.6276, 349.2282,
391.9954, 440.0, 493.8833, 523.2511]
MASTER_VOL = 1.0

def create_one_sample(a_sample_no, a_index, a_sample_list_left, a_sample_list_right,
base_sample_freq, target_sample_freq):
now_sample_no = a_index * CHUNK + a_sample_no
if now_sample_no < ATTACK: # --- ATTACK ---
vol = 1.0 - (ATTACK - now_sample_no) / ATTACK
elif now_sample_no < DECAY_POINT: # --- DECAY ---
vol = 1.0 - (1.0 - SUSTAIN_LEVEL) * (now_sample_no - ATTACK) / DECAY
elif now_sample_no < SUSTAIN_POINT: # --- SUSTAIN ---
vol = SUSTAIN_LEVEL
elif now_sample_no <= END_POINT: # --- RELEASE ---
vol = SUSTAIN_LEVEL * (END_POINT - now_sample_no) / RELEASE
else:
vol = 0.0
if vol < 0:
vol = 0.0 #誤差のための安全装置
elif vol > 1.0:
vol = 1.0 #誤差のための安全装置

#ReSampling Wave
target_sample_seq = 1.0 * a_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
float_data_l = sample_left * vol * MASTER_VOL
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)

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
float_data_r = sample_right * vol * MASTER_VOL
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)

return byte_data_l + byte_data_r

def create_data(a_chunk, a_index, a_sample_list_left, a_sample_list_right, a_freq):
rtn_sample = b''
for sample_no in range(a_chunk):
one_sample = create_one_sample(sample_no, a_index, a_sample_list_left,
a_sample_list_right, BASE_SAMPLE_FREQ, a_freq)
rtn_sample += one_sample
return rtn_sample

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 index in range(int(END_POINT/CHUNK) + 10):
wave_data = create_data(CHUNK, index, a_sample_list_left, a_sample_list_right, freq)
stream.write(wave_data)
stream.stop_stream()
stream.close()

if __name__ == '__main__':
pa = pyaudio.PyAudio()
sample = wave.open('KEPSREC045.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()

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

440Hzの音(880Hzかもしれない。。。)を基音に、リサンプリングして音をだしてみました。元ファイルの読み込みには少しだけ時間がかかりますが、リサンプリング演算はリアルタイムに1サンプルずつ行っても音が途切れませんでした。なお、元がピアノの音なので、ADSRのうち、AとDは使わず、そのままの音を出しています。

リリース(音がきえていく部分)は残響なしに無理やり音を絞っているので不自然ですが、確かにピアノみたいな音がします!!

サンプラー(リサンプリング)も問題なくリアルタイムでできました!

 

さて、明日のお題は(失敗が約束されていると感じている)1サンプル単位のリアルタイム「エフェクト」です。エコーくらいならうまくでしょうが、フィルタリングなどは周波数1周期分くらいの情報がモトネタで必要になると思うので、1サンプル出力するために扱う情報が、いままでの例の10~1000倍くらいにハネ上がります。

なんとなく区切りがいいので、一旦ページを終わります。

では、また。

おまけ

今日はまだ時間があるので、デジタルフィルタを予習します。

f:id:AssistantOfKoo:20210929215921j:plain

実装がとてもわかりやすいので参考になります!というか、まんま「これ」でいけそうな感じがします。流石、音系の先駆者でもある「断創録」様です。

実装としてはIIRとFIRフィルタ、種類でいえばバターワースフィルタとベッセルフィルタくらが理解できればよさげです。もう少しがんばれれば、チェビシェフフィルタもみてみます。

なるほど!遅延(nサンプリング前の値参照)と足し算(離散積分)と引き算(離散微分)と割り算(平滑化:平均算出に使用など)が基本なのですね。忘れてたけど、小さい時に習った気がします。

さらに余談

関係ないけど真空管も習いました、というより実習は真空管でした。前にも書きましたが、コンピュータ実習は「紙テープ」パンチし、「コアメモリ」(半導体メモリではない!)をもった「ミニコン」でFortran技術計算プログラミング(大砲の弾道計算)しました!

井戸から飛び出るかえる電子の計算や、電磁波の計算(マクスウェルの悪魔的方程式)はうっすら覚えてます(八木アンテナのゲイン計算が「まったく理解できなかったこと!」はよく覚えてます)が、これ(デヂタルフィルタ)は完全に忘れてました。(大学の教科書はたぶん全部捨てましたが、未整理で実家に残ってたりするかな?)

デジタルフィルタといい、量子力学といい、「趣味として!」もう一度勉強することになるとは、思いもよりませんでした。。。

 

まずはフィルタの計算特性は気にせず、いろいろ試してみます。