クーの自由研究

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

Pythonで音処理のリアルタイム性について実験する(お題)

やりはじめると「にわか」に調子づいてしまうことがあります

一瞬「ヤる気」をみせてみる、かえるのクーの助手の「井戸中 聖」(いとなか せい)です。

f:id:AssistantOfKoo:20210927214339j:plain f:id:AssistantOfKoo:20210927214940j:plain

難しいだろうけど、できるのかも!

メトロノームの実験プログラムをしていて、(昔のPCの感覚では「難しい」と感じていたことが、)最近のスペックのPCでは「もしかしてできちゃうのかも!?」と感じました。遅いイメージしかなかったpythonのループもPCスペックによっては結構高速に回ることがわかりました。

44.1KHz もしくは48.0KHz/16Bitくらいであれば、「1サンプリング」をじっくり計算して作成していっても、途切れずにずっと音を出せるのではないかと思った次第です。

f:id:AssistantOfKoo:20210927221423p:plain

今までの音系のプログラムは「出すべき音」の単位を「計算/準備してしまってから」ストリームに送出していましたが、計算しながら延々と「途切れずに」ストリーム出力できると考えた次第です。入力信号に対してエフェクト処理をして出力するのは、(ある程度「まとまり」で処理するので、)リアルタイムでも比較的簡単にできるのですが、なにもないところから、サンプリング1単位で音を創成する系は計算量がかなりあるので難しいと感じていました。

今週のお題

f:id:AssistantOfKoo:20210927222750j:plain

『Pythonで音の1サンプリング処理(に関するリアルタイム性)の限界や余裕を実験する』

にします。場合によってはNumbaやPyPy(コンパイル系)のお世話になるかもしれません。

 

最初の実験 「1サンプル毎の処理は間に合うのか?」

sin波を「1サンプル」(1/48.0K)ずつ!計算して、1チャンク分できたところでStreamに送出していき、きれいに音がでるか?(昔の「音のプログラム」では、こんなことは考えちゃだめだよ!といわれたやつ)

※普通はnumpyなどで何千もの「サンプル」を一度に計算しますが、1サンプルづつ処理するところがポイントです。なお、計算は32bit浮動小数点で行い、最終的には結果を16Bit整数化とするものとします。

f:id:AssistantOfKoo:20210927221110p:plain

さてやってみましょう。


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

CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 48000
FRQ_L = 440.0
FRQ_R = 659.255 #660.0
MAX_SIG = 32000
MAX_RANGE = 1000
#
def create_one_sample(a_sample_no, a_index):
for delay in range(20):
time.sleep(0.0) # Abount 12 micro sec 以内(とっても機種依存な数値です)
float_data_l = math.sin(FRQ_L * 2 * math.pi * (a_index * CHUNK + a_sample_no) / RATE) * MAX_SIG
byte_date_l = int(float_data_l).to_bytes(2, 'little', signed=True)
float_data_r = math.sin(FRQ_R * 2 * math.pi* (a_index * CHUNK + a_sample_no) / RATE) * MAX_SIG
byte_date_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_date_l + byte_date_r

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

def play(a_pa):
stream = a_pa.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
output=True)
for index in range(MAX_RANGE):
wave_data = create_data(CHUNK, index)
stream.write(wave_data)
stream.stop_stream()
stream.close()

if __name__ == '__main__':
pa = pyaudio.PyAudio()
play(pa)
time.sleep(1)
pa.terminate()

ステレオでそれぞれ別の周波数で鳴らしているので、右がE(ミ:平均律659.255Hz)、左がC(ラ:440Hz)です。聴力検査の雰囲気が味わえます。

マシンによっては音が途切れる(orでない)と思います。

time.sleep(0)の回数を増やすと処理時間が間に合わず、ノイズがでたり音が全くでなくなったりします。

f:id:AssistantOfKoo:20210928021603p:plain

この実験での、1サンプルあたりの作成に許される時間は、概ね20.8μSecなので相当に厳しいです。フェードイン・アウト(だんだん大きくなる・小さくなる)のコーディングをしようとしただけで音がでなくなりました!

上記プログラムの音です。

処理に時間がかかりノイズがではじめるとこんな感じになります。

以上より、Pythonで1サンプルつづデータを作成して(pyaudioで)鳴らすことは、そこそこのスペックのPCでは可能だと思います。

余談ですが、1サンプルデータは2チャンネルの場合、16Bit(符号付き)×2でバイト変換した情報になります。(なので1サンプル4バイト)お約束で、左、右、左、右・・・の順番で2バイト毎にデータをつないでいきます。

なお、昔遅くて悩んだ「バイト列の結合」は、いまやとっても高速です。

間に合いました!

ではまた明日!

今日(9/28)のお題は「音量制御」

『フェードイン/アウトは本当に間に合わないのか』です。

f:id:AssistantOfKoo:20210928223937j:plain

forループは超絶遅いイメージがあったのですが、この実験では善戦しています。

昨日は失敗しましたが、せめて音量調整くらいはやりたいものです。

あ、昨日NGだと思ったのは単なるコーディングミスでした。1チャンク(1024サンプル)内でフェードイン、フェードアウトしてました!

比較のため、片チャンネルだけ音量変化させていますが、こんな感じです。

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

MAX_RANGE = 400
TOTAL_SAMPLE = CHUNK * MAX_RANGE
FADE_IN_RANGE = TOTAL_SAMPLE * 3.0 / 10.0
FADE_OUT_RANGE = TOTAL_SAMPLE * 7.0 / 10.0
#
def create_one_sample(a_sample_no, a_index):
#for delay in range(40):
# time.sleep(0.0) # Abount 12 micro sec (とっても機種依存な数値です)
now_sample_no = a_index * CHUNK + a_sample_no
if now_sample_no < FADE_IN_RANGE:
vol = 1.0 - (FADE_IN_RANGE - now_sample_no) / FADE_IN_RANGE
elif now_sample_no > FADE_OUT_RANGE:
vol = 1.0 - (now_sample_no - FADE_OUT_RANGE) / (TOTAL_SAMPLE - FADE_OUT_RANGE)
else:
vol = 1.0
float_data_l = math.sin(FRQ_L * 2 * math.pi * (a_index * CHUNK + a_sample_no) / RATE) * MAX_SIG * vol
byte_date_l = int(float_data_l).to_bytes(2, 'little', signed=True)
float_data_r = math.sin(FRQ_R * 2 * math.pi* (a_index * CHUNK + a_sample_no) / RATE) * MAX_SIG
byte_date_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_date_l + byte_date_r

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

「1サンプル毎に」ボリューム計算(vol)をして掛けています。

f:id:AssistantOfKoo:20210928221311p:plain

 

結論は「処理が間に合いました」!エンベロープ(音量包絡)の制御もこれでOKです。

今日はまだすこし時間があるので、もう少し複雑なエンベロープの実験をします。

ADSRしてみました。右も同じエンベロープしてみました。

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

ATTACK = 1000     #SAMPLE TIME
DECAY = 2000 #SAMPLE TIME
SUSTAIN_LEVEL = 0.3 #LEVEL
SUSTAIN_TIME = 10000 #SAMPLE TIME
RELEASE = 5000 #SAMPLE TIME
DECAY_POINT = ATTACK + DECAY
SUSTAIN_POINT = ATTACK + DECAY + SUSTAIN_TIME
END_POINT = ATTACK + DECAY + SUSTAIN_TIME + RELEASE

def create_one_sample(a_sample_no, a_index):
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 #誤差のための安全装置
float_data_l = math.sin(FRQ_L * 2 * math.pi * (a_index * CHUNK + a_sample_no) / RATE) * MAX_SIG * vol
byte_date_l = int(float_data_l).to_bytes(2, 'little', signed=True)
float_data_r = math.sin(FRQ_R * 2 * math.pi* (a_index * CHUNK + a_sample_no) / RATE) * MAX_SIG * vol
byte_date_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_date_l + byte_date_r

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

f:id:AssistantOfKoo:20210928233809p:plain

 

サンプリング単位の音量制御ができました!

明日は、波形作成に戻って、矩形波、鋸歯状波(ノコギリ波)、サイン加算とFM変調演算くらいを予定しています。

ではまた明日!

今日(9/29)のお題は「いろいろな波形の生成」

いろんな波をつくってみます。

f:id:AssistantOfKoo:20210929024633j:plain

ノコギリ波とFM変調以外は簡単ですね。sin 関数部分をちょっといじればできます。

だんだん耳が痛くなる音となるので、せめてエンベロープは昨日のものを使います。(そのほうが耳にやさしいので)

矩形波

まずは矩形波です(凶悪な音なので音量注意!)

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

#Square Wave
sin_sample_l = math.sin(FRQ_L * 2 * math.pi * (a_index * CHUNK + a_sample_no) / RATE)
float_data_l = (1.0 if sin_sample_l > 0 else -1.0) * MAX_SIG * vol
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)
sin_sample_r = math.sin(FRQ_R * 2 * math.pi* (a_index * CHUNK + a_sample_no) / RATE)
float_data_r = (1.0 if sin_sample_r > 0 else -1.0) * MAX_SIG * vol
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_data_l + byte_data_r

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

f:id:AssistantOfKoo:20210929014017p:plain

オーディオインターフェースを介して「録音」しているので、計算通りの値ではなく多少波形は崩れています。(実はある程度崩れたほうが「味」のある音になったりします)

貼るのわすれてました; あまりにうるさいので、-8dbにしてアップしましたが、まだうるさいです。ご注意ください。

 

鋸歯状波

鋸歯状波(ノコギリ波)はやってみると逆に簡単でした。こちらも結構うるさいですので、音量注意です。

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

#SAW 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
sin_sample_l= (1.0 if (saw_val_l > 1.0) else (-1.0 if saw_val_l < -1.0 else saw_val_l)) #安全装置
float_data_l = sin_sample_l * MAX_SIG * 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
sin_sample_r= (1.0 if (saw_val_r > 1.0) else (-1.0 if saw_val_r < -1.0 else saw_val_r)) #安全装置
float_data_r = sin_sample_r * MAX_SIG * vol
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_data_l + byte_data_r

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

f:id:AssistantOfKoo:20210929013859p:plain

こちらは-8dbするのを忘れました。凶悪な音です。ご注意ください。

いずれも、サンプリング時点の瞬間時刻での該当波形の実質的な「位相」とその関数値を計算すればいいだけなのでとても簡単です。

倍音加算方式

弦をイメージしてください。ベンとはじくと「1/整数」整数は1,2,3... で弦が震えます。例えば3の場合は弦の1/3 のところが節のようになって振動します。これが3倍音です。弦はこのような倍音が多数重なって魅力的な音になります。波は重ね合わせてなんぼです!そして、楽器の構造上、倍音の取捨や、響き(エコー)などによって、楽器固有の音になります。ここでは倍音の合成のみを考えます。基本的に sin 波で考えます。

f:id:AssistantOfKoo:20210929085215p:plain

AとEの音は飽きてきたので、わずかにピッチの異なるA音にしてみます。

倍音の構成をリストで与えて音をつくってみました。

なお、左は440Hz, 右は444Hzにしています。(音に少し厚みがでるようになります:こういう重ね方は本来モノラルで行うべきなのですが、実験なので。。。)

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

def overtone_additional(a_base_frq, a_ary, a_sample_seq, a_rate):
float_data = 0.0
for ovt in range(len(a_ary)):
float_data += math.sin(a_base_frq * (ovt + 1) * 2 * math.pi * a_sample_seq / a_rate) * a_ary[ovt]
return float_data / sum(a_ary)

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

#Overtone Addition Method
float_data_l = overtone_additional(FRQ_L, [5,4,2,4,1,4,1,1,0.5,0.2], (a_index * CHUNK + a_sample_no), RATE) * MAX_SIG * vol
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)

float_data_r = overtone_additional(FRQ_R, [4,2,6,3,5,2,4,1,2,0.5], (a_index * CHUNK + a_sample_no), RATE) * MAX_SIG * vol
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)
return byte_data_l + byte_data_r

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

f:id:AssistantOfKoo:20210929022804p:plain

結構計算量が増えていますが、まだバッファ枯渇/途切れはしてないみたいです。

すこしブラスっぽい音になりました。

(音をここにはる)

 

f:id:AssistantOfKoo:20210929080548p:plain

16倍音めで、ちょうど4オクターブになります。あれ?と思われるかもしれませんが、2倍×2倍×2倍×2倍で、2の4乗で16倍です!。そして(上とは別の例ですが)440Hz(Aラの3倍音は440×3=1320Hzです。そして3倍音の(ラに対してミ)近似値は440×「2の12乗根の19乗」(すなわち19半音上)の1318.5102277Hzです。
うがった極端な見方をすればですが、バイオリンからすると調により周波数調整しないピアノは、いい加減な音程の妥協の楽器です。

全部1ページでやりたかったのですが、長くなってきたので、次のページにいきます。

次は矩形波のPWM(パルス幅変調)からです。

それではしばし休憩します。

次のページでお会いしましょう。