※工業大学生です.文章力に期待をしてはいけません.
※納得するものが完成するまでシリーズにする予定です(完成するかはわからない).
##概要
音声信号処理の勉強をしていて,何か練習としてoutputをしてみたいなと思い立ちましたので,シンセサイザー(らしきもの)をいちから作ってみようと思います.今回は,とりあえずどんな仕組みでも良いので鍵盤を叩いて音を出すところまで完成させます.
(なので,音声信号処理までは厳密には到達しない…)
##今回の目標
MIDIキーボードの入力を受け付けて,リアルタイム(記述済みのMIDIコードから音を生成するのではなく,キーボードを弾いた瞬間に音を出す)でその入力に対応した音を出力します.音は単音のみ(モノフォニック)で,音の強さ等の操作は受け付けません.エフェクトも使用しません.
設計
音を出す仕組みの部分と,MIDI入力,音声出力に分けて考えます.
音源
シンセサイザー(アナログシンセサイザー,またはそれをモデリングしたもの)の大まかなしくみは,以下の図のようになっています.
(図は参考資料[1]を元に作成)
VCO(Voltage Controlled Oscillator)が指定した音の高さの波形を出力し,VCF(Voltage Controlled Filter)でフィルタをかけて周波数特性を加工,VCA(Voltage Controlled Amplifier)で音量を調節します.
今回はエフェクトや音量調整の仕組みは入れないので,音を出すVCOの部分のみを実装します.VCOで発生させる波形の基本周波数を変えることによって,音階を実現します.
今回,波形は矩形波を用います.
基本周波数を$f_0$として,基本周期は$t_0 = 1/f_0$と表されます.このとき,矩形波は以下のような式で表されます.
s(n) = \left\{ \begin{array}{ll}
1 & (0 \leq n < t_0/2) \\
-1 & (t_0/2 \leq n < t_0)
\end{array} \right.
波形は,sin波が複数重ね合わせられた波形と考えることができます.具体的には,以下のように奇数次の倍音が重ね合わせられた式になります.($f_s$は標本化周波数)
s(n) = \sum_k \frac{1}{2k-1}sin(\frac{2\pi(2k-1)f_0n}{f_s})
音色としては,某配管工のゲーム音のような感じになります.
MIDI入力
MIDIデバイスからの信号をデコードし,基本周波数(音階)を計算します.(一番面倒)
MIDIデバイスからの信号はあるルールに沿って出力されているのですが,それらのルールを完全に把握してデコーダーを作成するのはなかなか骨が折れます.
そこで,今回は既に作成されたライブラリを使用することにします.
様々な言語でMIDIのライブラリ自体は存在するのですが,色々試作してみた結果,pythonのものが試しで楽しむ分には使い勝手が良さそうでした.
ライブラリは,pygameのものを使用します.
pygame.midiでは,以下のように入力を受け付けます.出力はA4(=440Hz)にあたる鍵盤を押したときを示しています.
midi_events = midi_in.read(10)
print ("full midi_events:" + str(midi_events))
#full midi_events:[[[144, 69, 74, 0], 1548]]
左の数字から,ノートオン・オフ,ノートナンバー,音量,チャンネル,タイムスタンプを表しています.
今回は鍵盤の判定と音の高さの判定のみを行いますので,左から2つの数値のみを使用します.
このノートナンバーと基本周波数の変換は,以下の式で行うことができます.
f_0 = concertpitch \times 2^{\frac{(notenumber-69)}{12}}
$concert pitch$はチューニングの基準音の周波数(一般的にはA4 = 440Hz),$notenumber$はMIDIから受け取った音の高さの信号です.
音声出力
pythonで,かつリアルタイムで音声出力ができるという条件で考えます.
今回は,pyaudioを用いることにしました.
##実装
VCO
矩形波を出力します.クラスとして定義しており,関数に三角波(今回は解説していません)などを追加すれば,シンセの音色自体を変えることもできます.
class VCO():
def __init__(self):
self.m = 0
self.data = [0] * CHUNK #初期化
def square(self,freq=220,amp=0.1):
t0 = int(RATE / freq)
for n in range(CHUNK):
if(self.m >= t0/2):
sign = 1
else:
sign = -1
s = amp * sign
self.m += 1
if(self.m >= t0):
self.m = 0
self.data[n] = int(s * 32767.0)
data_out = self.data
return data_out
MIDI入力
MIDIキーボードからの入力を受け付けます.ノートナンバーのみを取り出し,対応する音の基本周波数に変換します.このとき,ノートオン/オフの信号も受け取り,操作に応じて音量を変えることで音の有無を切り替えます.
MIDIデバイスを接続してからプログラムを実行します(自動認識されます).
midi_events = midi_in.read(10)
note_num = midi_events[0][0][1] #ノート番号
note_status = midi_events[0][0][0] #ノートオン・オフ信号
if(note_status == 128):
if(past_note_num == note_num):
amp = 0 #音量0で音をoff
past_note_num = 0
note_num = 0
elif(note_status == 144):
amp = 0.1 #音をon
concert_pitch = 440 #チューニング
freq = concert_pitch * (2**((note_num-69)/12)) #基本周波数へ変換
past_note_num = note_num
data_vco = vco.square(freq=freq,amp=amp)
波形出力
pyplotでリアルタイムで出力している波形を表示してみます.基本周波数が変わる様子がよく分かります.(少し処理が重たいので,演奏を楽しみたい方はコメントアウト推奨)
#plotの準備
fig, ax = plt.subplots(1, 1)
x = np.arange(0,CHUNK,1)
y = np.zeros(CHUNK)
lines, = ax.plot(x,y)
ax.set_ylim(-2**12,2**12)
lines.set_data(x,data)
plt.pause(0.00001)
音声出力
MIDI入力に応じて,VCOから出力した波形を,音声として出力します.pyaudioのstreamにバイナリに変換して出力します.
mainコード全体
RATE = 44100
CHUNK = 512
if __name__ == '__main__':
#plotの準備
fig, ax = plt.subplots(1, 1)
x = np.arange(0,CHUNK,1)
y = np.zeros(CHUNK)
lines, = ax.plot(x,y)
ax.set_ylim(-2**12,2**12)
p = pyaudio.PyAudio()
#for midi device
pygame.midi.init()
input_id = pygame.midi.get_default_input_id()
print("input MIDI:%d" % input_id)
print("ready to play!")
midi_in = pygame.midi.Input(input_id)
stream = p.open(format=pyaudio.paInt16,channels=1, rate=44100, output=1)
m = 0 #初期値
note_num = 0
freq = 440
amp = 0
past_note_num = 0
vco = VCO()
while True:
if midi_in.poll():
midi_events = midi_in.read(10)
print ("full midi_events:" + str(midi_events))
note_num = midi_events[0][0][1] #ノート番号
note_status = midi_events[0][0][0] #ノートオン・オフ信号
if(note_status == 128):
if(past_note_num == note_num):
amp = 0 #音量0で音をoff
past_note_num = 0
note_num = 0
elif(note_status == 144):
amp = 0.1 #音をon
concert_pitch = 440 #チューニング
freq = concert_pitch * (2**((note_num-69)/12)) #基本周波数へ変換
past_note_num = note_num
data_vco = vco.square(freq=freq,amp=amp)
data = np.array(data_vco,dtype=int)
lines.set_data(x,data)
plt.pause(0.00001)
data = struct.pack("h" * len(data), *data) #バイナリ変換
stream.write(data)
要改善点
- 音を出しているだけなのにCPU使用率が異常に高い.
- 波形出力は面白いけれど,動作が明らかに重そう.
- やや応答が思い通りにいかないときがある.MIDI入力に対するノートオン/オフのアルゴリズムを調整するか,並列処理を考える必要がありそう.
##おまけ(MIDIキーボードが無い方のために)
キーボードの数字入力部分で音が出せるコードも作ってみました.
チャルメラくらいなら弾けるかもしれません.
from blessed import Terminal
if __name__ == '__main__':
p = pyaudio.PyAudio()
t = Terminal()
stream = p.open(format=pyaudio.paInt16,channels=1, rate=44100, output=1)
m = 0 #初期値
note_num = 60
vco = VCO()
with t.cbreak():
while True:
k = t.inkey(timeout=0.000001)
if(k.name == 'KEY_ESCAPE'):
break
if(str.isdigit(k)):
note_num = int(k) + 60
concert_pitch = 440
freq = concert_pitch * (2**((note_num-69)/12))
print(freq, k, end="\r")
data = vco.square(freq = freq, amp = 0.1)
data = struct.pack("h" * len(data), *data) #バイナリ変換
stream.write(data)
まとめ
プログラミング系の記事で,ノートとかキーボードとかコードとか書くと,どっちの話をしているのかよく分からなくなりますね.
##参考資料など
[1]サウンドプログラミング入門,青木直史著,技術評論社
[2]MIDIキーボードから入力してみる(python,pygame)
[3]matplotlibでリアルタイム描画
自作シンセシリーズ
- この記事
- (未定)