【Python】マウスがインターフェースのテルミン

# 背景
　私は、Kensingtonのトラックボールを愛用しています。「トラックボール使いやすいですか？」と聞かれた時に「慣れたら楽だけど、テルミンみたいな感じかなぁ」と返答することがありました。本当にテルミンか？！ということを検証するために、マウス位置で音高と音量が変化するようなプログラムを作ってみました。

# 環境設定
以下の5つのモジュールを使用します。

```python
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は音量幅です。


```python
RATE = 44100 #サンプリング周波数
CHUNK = 1024 #バッファー
PITCH = 442 #ピッチ
minfreq = PITCH/2*2**(3/12)
tonerange = 2
period = 0.01
minamp = 0.1
amprange = 0.5
```


# 位置検出 
まず、ウインドウサイズを得ます。

```python
def get_window_size():
    window_size = pyautogui.size()
    return window_size
```

カーソル縦方向の移動で音高、横方向の移動で音量が変わる設定にします。画面の相対位置を計算します。音高は画面一番下が最低音、画面一番上が最高音とします。一方、音量は画面中央が最小、両端が最大になるようにします。カーソルがx軸（横方向）、y軸（縦方向）のどの位置にいるかを相対位置で取得します。

```python
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
```

画面位置と音高、音量を対応させます。

```python
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波よりノイズのほうが目立ってしまいます。

```python
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()
```