クーの自由研究

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

Pythonで基本メトロノーム機能(実験用)を作ってみる

この記事は個人的Pythonの再×n入門の記事です

やっぱりPython🐍がとっても面白いと思う、かえるのクーの助手の「井戸中 聖」(いとなか せい)です。

f:id:AssistantOfKoo:20210926162129j:plain

忘れていても、文法をしらべながらサクサク組める&すぐ意図したとおり動く!のはとても素敵です。

いつものように組みながら書いていきます。(途中ひっかかった情報のほうが重要なのですが、プログラムが完成したときにはすっかりそれを忘れているので、自分にはこのスタイルが合っています)

 

タイミングに関するデザイン

練習用のプログラムなので、設計は「なし」で問題ないのですが、「音」はプログラムできれいに出すのがすこし厄介なので、あらかじめ最小限のことを考えます。

・メトロノームの音の最小要素はBPM(Beat per Minite)であると思われがちですが、高機能(電子系)メトロノームは裏拍や3連、16分、5連などの連符補助拍出力機能を持つものもあるので、そんなことをやりたいときはもっと細かい制御が必要です。

個人的にはBossのDB-90がとてもいいと思いますので、デザインの参考にします。

♩= 60 で 5連と6連をちゃんとならそうとすると音の間隔は実質的に0.025Sec(=2400BPM)くらいがちゃんと出せる必要があります。

・音を鳴らす方法はいろいろありますが、今回はPyAudioを使おうと思います。Streamに順次情報を出力することになります。この「順次」というのが厄介で、短すぎると(他の処理が滞ると)次の情報が期限まで設定できずバッファが枯渇して音切れorエラーとなります。長すぎると出音は安定するものの、バッファ設定処理自体の時間が無視できなくなり他の処理とのバランスが悪くなる(タイミングがずれる)ので、この設定はとても重要です。

サンプリング周波数が44.1KHzのデータを使うとすれば、441000×0.025=1102.5なので切りよく512か1024くらいが候補になります。

ただし、他の処理の関係で音が途切れたりノイズが入る場合は調整が必要です。

 

プログラミング

マルチスレッドにしますが、pythonはマルチスレッドが苦手(疑似的なマルチスレッド)で、PyAudioもstreamの入り口は(結局内部で)1つなので、マルチスレッドにする意味は薄いかもしれません。


#!/usr/bin/env python
# -- coding: utf-8 --
import time
import pyaudio
import wave
import threading

CHUNK = 8192

class ToneThread(threading.Thread):
def __init__(self, aPa, aWf):
super(ToneThread, self).__init__()
self.wf = aWf
self.p = aPa
self.wf.rewind()
self.data = self.wf.readframes(CHUNK)
self.stream = self.p.open(format=pyaudio.paInt16,
channels=2,
rate=int(aWf.getframerate()),
output=True)
self.stop_event = False

def run(self):
while len(self.data) > 0:
self.stream.write(self.data)
self.data = self.wf.readframes(CHUNK)
if self.stop_event:
print("stop!") # for debug
self.data = b''
self.stream.stop_stream()
self.stream.close()
self.wf.rewind()
time.sleep(0.0)
self.data = self.wf.readframes(CHUNK)

def stop(self):
self.stop_event = True
pass

def pattern_factory(pattern, num, aWf1, aWf2, aWf3):
if pattern == 1: # 1Beat
rWf = (aWf3, None, None, None)[num % 4]
elif pattern == 2: # 2Beat
rWf = (aWf1, None, aWf2, None)[num % 4]
elif pattern == 3: # 3 Odd Beat
rWf = (aWf1, None, None, aWf3, None, None, aWf3, None, None,
aWf2, None, None, aWf3, None, None, aWf3, None, None)[num % 18]
elif pattern == 4: # 4 Beat
rWf = (aWf1, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf3,
aWf2, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf3)[num % 16]
elif pattern == 5: # 5 Odd Beat
rWf = (aWf1, None, aWf3, None, aWf3, None, aWf3, None, aWf3, None,
aWf2, None, aWf3, None, aWf3, None, aWf3, None, aWf3, None)[num % 20]
elif pattern == 6: # 6 Beat
rWf = (aWf1, aWf3, aWf3, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf3, aWf3, aWf3)[num % 12]
elif pattern == 9: # 9 Odd(4-3-2) Pattern
rWf = (aWf1, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf2, aWf3)[num % 9]
elif pattern == 10: # The TARKUS(4-3-3) Pattern
rWf = (aWf1, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf2, aWf3, aWf3)[num % 10]
else: # 4Beat
rWf = (aWf1, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf3, aWf2, aWf3, aWf3, aWf3)[num % 16]
return rWf

def play(pa, wf1, wf2, wf3):
pattern = 10
bpm = 120.0
timing = 60.0 / (bpm * 4)
test_range = 10 * 8
offset = 0.2
adj_timing = timing - offset if (timing - offset) > 0 else 0.001
pre_timing = 0
start_time = time.perf_counter()
diff_max = 0.0
diff_min = 1.0
diff_total = 0.0
last_th_cl = ToneThread(pa, wf1) # First dummy class
for num in range(test_range):
print(num)
wf = pattern_factory(pattern, num, wf1, wf2, wf3)
if wf is not None:
th_cl = ToneThread(pa, wf)
time.sleep(adj_timing)
diff = 0
for timing_loop in range(10000000):
now_timing = time.perf_counter()
elapsed = now_timing - start_time
expect = timing * (num + 1)
diff = expect - elapsed
if diff < 0.0000001 or elapsed > expect:
print(now_timing - start_time)
adj = now_timing - (pre_timing + timing)
adj_timing = timing - adj - offset
if adj_timing < 0.001:
adj_timing = 0.001;
print("MainLoop={:d}, elapsed={:f}, expect:{:f}, adj={:.10f}, timing_loop:{:d}"
.format(num, elapsed, adj_timing, expect, timing_loop))
break
elif diff > 0.000012 and diff < 0.03:
# print("A elapsed:{:.10f}, expect:{:f}".format(elapsed, expect))
time.sleep(0) # Abount 12 micro sec
elif diff >= 0.03:
# print("B elapsed:{:.10f}, expect:{:f}".format(elapsed, expect))
time.sleep(0.000001) # Abount 16 milli sec
diff_total += diff
if diff > diff_max:
diff_max = diff
if diff < diff_min:
diff_min = diff
pre_timing = now_timing
elapsed = now_timing - start_time
expect = timing * (num + 1)
# print("elapsed:{:.10f}, diff:{:.10f}".format(elapsed, elapsed - expect))
if wf is not None:
last_th_cl.stop()
th_cl.start()
last_th_cl = th_cl
print("result:diff_max={:.10f}, diff_min={:.10f}, ave={:.10f}".format(diff_max, diff_min, diff_total / test_range))

if __name__ == '__main__':
print("Start Metronome Odd(Taxi)Meter V0.01")
print('call play()')
wf1 = wave.open('BellClick1.wav', 'rb')
wf2 = wave.open('Click1.wav', 'rb')
wf3 = wave.open('Click2.wav', 'rb')
pa = pyaudio.PyAudio()
play(pa, wf1, wf2, wf3)
time.sleep(1)
pa.terminate()

練習用なのでベタベタなコーディングです。

実行されたい方は好みのフリーWaveファイルをネットから拾ってきて、0.2Sec程度の長さに編集してお使いください。

簡単な説明

改造したい人用と未来の自分用の情報です。読み飛ばしてください。

・音出し部分を単発のスレッドオブジェクトにまかせています。音1個につき1オブジェクトができるので、きわめて効率がわるいです。他の処理に依存せず、タイミングを正確に取りたいためこの方法にしていますが、他に簡単に行う良い方法を思いつけません。

・本来なら、stream出力部分は1つなのでこの部分はシングルスレッドにし、再生直前のStreamを(2~10チャンク分)Streamを合成生成していけば、タイミングも出音(きれいさ、重ね合わせともに)完璧になると思いますが、それこそ44.1KHz(もしくは48KHz)の1つ1つのデータを「紡いで」いくことになるので、とてもシビアなコーディングになるとおもわれます。

・おおまかなタイミング(秒オーダ)はSleepでとっています。BPMよりも少し短い時間を設定しています。処理時間があるので、すこし補正しながら行います。

・マスタのタイマに合わせるようにタイミングをとります。仮に一度すこしズレたとしても、補正していくしくみです。

・やや細かめのタイミングはSleepの最小単位≒「タイマ」精度(15mSecオーダ)でとります。音出ししたいタイミングの 30msec手前に近づくまではこの微小Sleepで行います。なお、時刻・タイミングの判定は高精度のtime.perf_counter()で行っています。

・30mSec以下で12μSecより大きい範囲(手前)ではSleep(0)でμSec単位の頭出しをします。Sleep本来の動作ではありませんが、CPUをLoopさせるよりは健明だと考えました。

・所定の時刻まで12μSec以下の場合はCPUのLOOPで該当タイミングの0.1μSec以内、もしくはこえるまで(超えるのはイレギュラー)になるように頭出しします(タイミングを待ちます)

・該当の時刻タイミングになれば、音出しの命令を発行します。

以上で、精度が0.1μSec程度のタイミングをとることが可能です。音だけではなく他の用途にも汎用的に使える方法だと思います。

ただ、出音となると聞いてすぐわかるほど確実にゆらいでいるので微妙です。(確実にわかるくらいなので、MidiやASIOで苦労した過去の経験上±30mSecはゆらいでると思います)

失敗の記録

f:id:AssistantOfKoo:20210926162812p:plain

事前の考察ではチャンク(CHUNK)=:バッファの設定単位 は 1024がよさそうでしたが、やってみるとノイズ出まくり、音切れまくりで安定しませんでした。

CHUNK=1024の例

いろいろな値の設定の結果、CHUNK=8192ぐらいならまぁ「聞ける」くらいにはないのでこの値にしました。

CHUNK=8192の例

 

結果は「ギリ」実用にならない!程度かと思われます。

音のミキシングはいろいろ調べたのですが、ストリームに出力する前に合成演算はしておかなければならないようで、複数の音を同時に鳴らせるようになってません。

これが実現できれば完全実用とできるでしょう。

がんばれば作れそうですが、(即ち頑張らなければ決して作れないので)今回はそこまで時間がありません。妥協します。

やっぱり「音」系のリアルタイムは難しいです。

出音の例

変拍子・奇数表示パターンも作ってみました。音の例は以下な感じです。

1拍子

2拍子(60って書いたけど 120BPMですね)

 

3拍子

 

4Beat

 

5拍子

 

6拍子

 

9拍子(4-3-2)

 

10拍子(4-3-3)

 

「出音指定のタイミング」はどれも±0.1~0.2μSecの誤差なのですが、出音自体は音発生までの処理のばらつきがあるためか、私でもわかるくらい微妙に揺れております。

わたくしの下手なギターの練習用途には充分な精度かと思いますので、これ以上は追及しません。より正確性を求めるなら音出し部分は「別プロセス」かMIDI機器にやらせたほうが正解だと思われます。

ようこそ妖の住処へ

変拍子とかポリリズムに「はまる」と一生抜け出せなるくらい「ここちよい」です。

5-5-7 みんな大好きパットメセニー

プログレ変拍子グルーブが神の領域、ビルブラフォード

ポリリズムは分析しようとせず、ただリズムに身をゆだねるととても気持ちいいですね~。

変拍子もポリリズムも「気が付かないくらい自然」なのがいい感じですね。

結論

メトロノームとして、そこそこの精度が得られましたので、次に時間ができたときにQTでGUI部分をコーディングして実用に耐えるものにしたいと思います。

再来週くらいから「また」超絶忙しくなるので、がんばります!