背景
私は、Kensingtonのトラックボールを愛用しています。「トラックボール使いやすいですか?」と聞かれた時に「慣れたら楽だけど、テルミンみたいな感じかなぁ」と返答することがありました。本当にテルミンか?!ということを検証するために、マウス位置で音高と音量が変化するようなプログラムを作ってみました。
環境設定
以下の5つのモジュールを使用します。
import pyaudio
import numpy as np
import pyautogui
import keyboard
from scipy import arange, cumsum, sin, linspace
from scipy import pi as mpi
pyautoguiは、今回はマウスの位置情報を取得するために使用します。pipのバージョンが20.2.1の場合、インストールエラーとなるようです(2020.8.11現在)。
一緒にインストールされるpymsgboxをダウングレードして手動でインストールして回避しました。ちなみに、サブ機はpip自体が前のバージョンだったのですが、その場合、エラーを出さずにインストールできました。
(前回記事 https://qiita.com/Lemon2Lemonade/items/0a24a4b65c9031536f6e)
pyaudioは、pythonで音を鳴らすモジュール、keyboardはキーボードの入力値を得ます。
入力値
RATEはいわゆるサンプリング周波数で、今回はCDと同じ44.1kHzにしてみました。
minfreqは最低音の振動数。442HzのAから1オクターブ下がって短3度上がったCです(平均律では、振動数に2の12乗根を掛けると半音上がります)。
tonerangeはテルミンの再生範囲。本文中ではminfreq*tonerangeのように周波数を定数倍するようにしていますので、2倍ということは1オクターブを意味します。
periodは、音の持続時間です。カーソルの位置検出を0.01秒ごとに行います。
minampは音量の最小値、amprangeは音量幅です。
RATE = 44100 #サンプリング周波数
CHUNK = 1024 #バッファー
PITCH = 442 #ピッチ
minfreq = PITCH/2*2**(3/12)
tonerange = 2
period = 0.01
minamp = 0.1
amprange = 0.5
位置検出
まず、ウインドウサイズを得ます。
def get_window_size():
window_size = pyautogui.size()
return window_size
カーソル縦方向の移動で音高、横方向の移動で音量が変わる設定にします。画面の相対位置を計算します。音高は画面一番下が最低音、画面一番上が最高音とします。一方、音量は画面中央が最小、両端が最大になるようにします。カーソルがx軸(横方向)、y軸(縦方向)のどの位置にいるかを相対位置で取得します。
def distance():
window_size = get_window_size()
x, y = pyautogui.position()
x_dist = np.sqrt((x-window_size.width/2)**2)
y_dist = abs(y-window_size.height)
x_max = np.sqrt((window_size.width/2)**2)
y_max = window_size.height
x_ratio = x_dist/x_max
y_ratio = y_dist/y_max
return x_ratio, y_ratio
画面位置と音高、音量を対応させます。
def tonemapping():
ratio = distance()
freq = minfreq*2**(tonerange*ratio[1])
return freq
def ampmapping():
ratio = distance()
amp = minamp+amprange*ratio[0]
return amp
sin波(音源)の調整と再生
以外に苦労したのがここの部分。位置に対して、ある音を一定期間鳴らすだけだと、音がぶつぶつ切れてノイズが乗ってしまいます。なので、ループのひとつ前の条件(音高とsin波の振動数、位相、振幅)を記憶しておいて、滑らかにつながるようにします。
次のサイトを参考に作りました。
http://yukara-13.hatenablog.com/entry/2013/12/23/053957
位相も合わせておかないと(一つのつながったsin波にしないと)「ブツッ」っというようなノイズになりますを。再生期間を0.01秒で計算するとsin波よりノイズのほうが目立ってしまいます。
def make_time_varying_sine(start_freq, end_freq, start_A, end_A, fs, sec, phaze_start):
freqs = linspace(start_freq, end_freq, num = int(round(fs * sec)))
A = linspace(start_A, end_A, num = int(round(fs * sec)))
phazes_diff = 2. * mpi * freqs / fs # 角周波数の変化量
phazes = cumsum(phazes_diff) + phaze_start # 位相
phaze_last = phazes[-1]
ret = A * sin(phazes) # サイン波合成
return ret, phaze_last
def play_wave(stream, samples):
stream.write(samples.astype(np.float32).tostring())
def play_audio():
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paFloat32,
channels=1,
rate=RATE,
frames_per_buffer=CHUNK,
output=True)
freq_old = minfreq
amp_old = minamp
phaze = 0
while True:
try:
if keyboard.is_pressed('q'):
stream.close()
break # finishing the loop
else:
freq_new = tonemapping()
amp_new = ampmapping()
tone = make_time_varying_sine(freq_old, freq_new, amp_old, amp_new, RATE, period, phaze)
play_wave(stream, tone[0])
freq_old = freq_new
amp_old = amp_new
phaze = tone[1]
except:
continue
if __name__ == '__main__':
play_audio()