質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

1759閲覧

pygameを用いたMIDIの制御

退会済みユーザー

退会済みユーザー

総合スコア0

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

1グッド

0クリップ

投稿2018/02/16 00:30

編集2018/02/16 08:41

###実現したい内容
pygameを用いてMIDIデバイスの入力を受け取り、リアルタイムでSin波で演奏
複数音の出力および、ベロシティの対応

発生してる問題

キーを押した時と離した時にプツプツとノイズが発生していしまいます。
音声のサスティン中に問題はありません。
色々と試してみたのですがうまくいきません
pygame.midi.Input.readの処理時間の問題でしょうか?
ご教示願います

コード

python

1#import部================================= 2import pygame.midi 3 4import math 5import numpy as np 6import pyaudio 7import struct 8#========================================= 9""" 10""" 11#pyAudip制御関連============================= 12if __name__ == '__main__': 13 p = pyaudio.PyAudio() 14 stream = p.open(format=pyaudio.paInt16,channels=1, rate=44100, output=1,stream_callback=None )# ストリームを開く 15 pos = 0 #posは波形生成時のタイムスタンプ 16 17 18#Midi制御関連============================= 19 20 pygame.init() 21 pygame.midi.init() 22 input_id = pygame.midi.get_default_input_id() 23 i = pygame.midi.Input(input_id) 24 25#----------------------------------------- 26 27def End(): #終了処理(必ず入れる) 28 i.close() 29 pygame.midi.quit() 30 pygame.quit() 31 stream.close() 32 p.terminate() 33 34#========================================= 35 36 37 38#Key管理================================ 39""" 40Key情報 41音程 0~127 まで オクターブごとに+12 42鍵盤を押したときの強さ 0~127 まで !0のときは鍵盤が押されていない 43 44keyID: 144 = 通常の鍵盤 45 [176,7,x,0] = Volume xは 0~127 まで 46     47""" 48#押されてるキーを保持するaリスト 49key_list = [] 50 51def Key_input(): 52 if i.poll(): 53 midi_events = i.read(1) #一度すべてのイベントを取り出す 54 state = midi_events[0][0] #読み取る値の取り出し 55 56 if 144 == state[0]: #鍵盤かの判定 57 if 0 != state[2]: #押された強さ 58 key_list.append(state[1:3]) 59 else : #離れたとき 60 for item in range(len(key_list)): 61 if key_list[item][0] == state[1]: 62 del key_list[item] 63 break 64 65 66 67 68#========================================= 69""" key_listには[MIDIノーツ番号,ベロシティ]""" 70 71#周波数とか作るところ======================= 72 73 74def ToNote(): 75 76 note_list=[] #notelist 初期化 77 78 for item in range(len(key_list)): 79 f = round(440.0 * (2**((key_list[item][0]-69)/12)) ,5) #周波数計算 80 a = round(key_list[item][1]/127.0,3) #音量計算 81 note_list.append([f,a]) #Listに追加 82 83 return note_list 84 85 86 87#========================================= 88 89 90 91#実際の波形を作成========================= 92def createData(freqList = [], start_pos=0): #オシレーター 93 data = [] 94 print(freqList) 95 if len(freqList)!= 0: 96 97 amp = 1.0 / len(freqList) #使用時は波形データにampを乗算する 98 99 end_pos = start_pos + 0.01 * 44100 #タイムスタンプ 100 101 for n in np.arange(start_pos, end_pos): 102 s = 0.0 #波形データをゼロクリア 103 for item in range(len(freqList)): 104 f = freqList[item][0] 105 s += amp * np.sin(2 * np.pi * f * n / 44100)*freqList[item][1] 106 # 振幅が大きい時はクリッピング 107 if s > 1.0: s = 1.0 108 if s < -1.0: s = -1.0 109 data.append(s) #末尾に追加 110 data = [int(x * 32767.0) for x in data] #値を32767~-32767間にする 111 112 113 # バイナリに変換 114 data = struct.pack("h" * len(data), *data) # listに*をつけると引数展開される 115 116 else : 117 118 end_pos = 0 #音がならない場合はタイムスタンプを0に 119 120 return data, end_pos 121 122#==================================== 123 124 125#再生================================ 126def play(stream,data): #再生用関数、ストリームと波形データを引数に 127 128 # チャンク単位でストリームに出力し音声を再生 129 chunk = 1048 130 sp = 0 # 再生位置ポインタ 131 buffer = data[sp:sp+chunk] 132 while buffer: 133 stream.write(buffer) 134 sp = sp + chunk 135 buffer = data[sp:sp+chunk] 136 137#========================================== 138 139 140#Main部==================================== 141if __name__ == '__main__': 142 143 144 while True: 145 146 Key_input() #キーの取得 147 148 note_list = ToNote() #押されているキーリストからノートのリストを作成 149 150 data, pos = createData(freqList = note_list,start_pos=pos) #波形の生成 151 play(stream,data) #出音 152 153 End() 154
退会済みユーザー👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

振幅の変化が激しすぎる(0からいきなり最大になったりその逆になる)のが原因と思います。

  • 無音からいきなり最大振幅にすると(多分アンプの特性によって?)音割れする
  • 最大振幅からいきなり振幅0にしても同じ
  • 同時発音数が変化した際にampを突然変化させると波に連続性がなくなるため音割れする

同時発音数というのは和音を再生する際には結構変化するので、本式のシンセサイザー(あるいはミキサー)には波形を調整するコンプレッサーとかリミッターと呼ばれるものが備わっています。これはご質問のコードにあるように単に範囲外をclampするという方法では実現できません。そうやってしまうと波形がひずみ結局音割れします。

波形を直接生成するような場合、こうした音の特性に注意する必要があります。自然界の音はエンベロープに従って振幅が徐々に変化するのが普通ですので、ソフトウェアで波形を生成する場合でも最小限のエンベロープ制御を行い波形が不連続にならないようにするとよいでしょう。

なお、基本体なエンベロープ制御は以下のような感じに考えるとよいと思います。

アタック:振幅0から最大までになる時間
ディケイ:振幅最大から定常状態の振幅(最大よりちょっと小さい)まで下がる時間
サステイン:振幅が定常状態のまま鳴り続ける時間(この時間が一番長い)
リリース:振幅が定常状態から0まで下がる時間

参考:https://www.g200kg.com/jp/docs/dic/attack.html

アタック~リリースの各振幅変化は自然音では複雑な曲線になったりするでしょうが、とりあえず線形に変化させると少なくとも音割れはせずにそこそこ自然に聞こえると思います。

プログラムの設計的な面について考えると、一つ一つのNOTE(特定の音色の特定のピッチ)に対して独立したオシレーターインスタンスを用いることにして、NOTE_ONの瞬間に新たな初期状態のオシレーターを生成し、オシレーターが時間変化によって適切な振幅で波形を生成し、NOTE_OFFでリリースが始まり振幅が完全に0になったらオシレーターインスタンスを消去するといった方法論が考えられます。(自分はJavaの標準ライブラリーのシンセサイザーぐらいしか見たことないですが、それはこういう手法を取っていました)
そのようなオシレーターを複数保持しておき、一定時間ごと(ご質問にあるコードですと0.01秒ごと)にその時点で存在している全てのオシレーターの波形を合成して結果の波形を生成するイメージです。

なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、滑らかに調整というのがなかなかめんどくさいのです。そこで、とりあえずは同時発音数を適当な値に固定してしまい(例えば8とか)、演奏する際にその限界を超えて発音するような事態を避けるのが簡単でしょう。同時発音数というのは同時にNOTE_ONとなっている数の最大数よりも大きくなる点に注意してください。NOTE_OFFしてもリリースタイムの期間の間振幅を徐々に小さくしながら音が鳴り続けるため「音が完全に消えるまでには若干時間がかかる」からです。


追記:実際に単純なエンベロープにしてやってみた例を挙げてみます。少々長いのでちょっと気が引けますが。自分の環境ではMIDI-INがすぐに使えないので、Key_inputの部分は自動演奏に置き換えています。質問者さんの環境ではこの関数を書き替えずに実行すれば演奏の通りに音が鳴らせると思います。(すみませんが、自動演奏に変えたため元のコードからpygame.midiの部分のコードは削除してあります)

下記コードでは0.4秒ごとにC4~B#4までの音を順に自動演奏していますが、その限りでは雑音はでないようでした。しかし・・・エンベロープを滑らかにしたつもりかつ同時発音数10固定なんですが、自動演奏で和音にしたとたん演奏開始時点で雑音が・・・

単なるバグなのかも知れませんし、Pythonのスピードとか自分の配慮が不足している点が何かあるのかも知れません・・・

追記2:PyCharm上で実行していたのですが、ふと思いついて「デバッグ実行」ではなく普通の「実行」にしたら和音にしても雑音でなくなりました!
想像ですが、PyCharmでデバッグ実行していたためインタープリタの実行スピードが音声再生においついてなかったのだと思います・・・

Python

1import math 2import pyaudio 3import struct 4import time 5 6# PyAudio制御関連============================= 7SAMPLE_RATE = 44100 8 9if __name__ == '__main__': 10 p = pyaudio.PyAudio() 11 # ストリームを開く 12 stream = p.open(format=pyaudio.paInt16, channels=1, rate=SAMPLE_RATE, output=1, stream_callback=None) 13 14 15def End(): # 終了処理(必ず入れる) 16 stream.close() 17 p.terminate() 18 19 20# Key管理================================ 21 22key_list = [] # 押されてるキーを保持するリスト 23start_time = time.time() # MIDI-INなしで自動演奏するための小細工 24 25 26def Key_input(): 27 # MIDI-INなしで自動演奏するための小細工 28 # MIDI-INが使えるならオリジナルのコードそのままでよいはず・・・ 29 global key_list 30 t = (time.time() - start_time) 31 if 3 < t < 20: 32 key = 60 + int((t - 3) / 0.4) % 12 33 key_list = [[key, 110]] 34 # 以下のように、3度の和音にしたら同時発音数=10でも音割れした。(Win10 64bit) 35 # =>PyCharmのデバッグ実行では音割れしたが通常実行にすると音割れしなくなった 36 # --------------------------------------------- 37 # key_list = [[key, 120], [key + 4, 100]] 38 else: 39 key_list = [] 40 return 41 42 43# 周波数とか作るところ======================= 44 45 46class Oscillator: 47 def __init__(self, key, max_level, sustain_level, attack_time, decay_time, release_time): 48 self.key = key 49 freq = 440.0 * (2 ** ((key - 69) / 12)) 50 self.dt = math.pi * 2 * freq / SAMPLE_RATE 51 self.max = max_level 52 self.sustain = sustain_level 53 self.attack_t = int(attack_time * SAMPLE_RATE) 54 self.decay_t = int(decay_time * SAMPLE_RATE) 55 self.release_t = int(release_time * SAMPLE_RATE) 56 self.released = None 57 self.time = 0 58 self.amp = 0 59 60 def calc_amp(self): 61 if self.released is None: 62 # まだNOTE_ON状態 63 if self.time < self.attack_t: 64 return Oscillator.interpolate(self.time, self.attack_t, 0.0, self.max) 65 else: 66 return Oscillator.interpolate(self.time - self.attack_t, self.decay_t, self.max, self.sustain) 67 else: 68 # NOTE_OFF状態 69 return Oscillator.interpolate(self.time - self.released, self.release_t, self.sustain, 0.0) 70 71 # 振幅の直線補間用 72 @staticmethod 73 def interpolate(cur_t, max_t, start_level, end_level): 74 t = cur_t / max_t if cur_t < max_t else 1 75 return start_level * (1 - t) + end_level * t 76 77 def wave(self): 78 self.amp = self.calc_amp() 79 w = self.amp * math.sin(self.time * self.dt) 80 self.time += 1 81 return w 82 83 def note_off(self): 84 if self.released is None: 85 self.sustain = self.amp # attack/decay中にNOTE_OFFされた場合への配慮 86 self.released = self.time 87 return True 88 else: 89 return False 90 91 def is_playing(self): 92 return self.released is None or self.time < self.released + self.release_t 93 94 95class Synthesizer: 96 def __init__(self, polyphony=8): 97 self.oscillators = {} 98 self.polyphony = polyphony 99 buffer_size = 1048 // 2 # 44100 1sec 4410 0.1sec 441 0.01sec 100 latency = buffer_size / SAMPLE_RATE 101 self.wave = [0] * int(latency * SAMPLE_RATE) 102 self.pack_format = 'h' * len(self.wave) 103 print("polyphony=", self.polyphony, "latency=", latency) 104 105 def generate_wave(self, keys): 106 """実際の波形を生成""" 107 self.update_oscillators(keys) 108 wave = self.wave 109 # print(keys) 110 amp = 32767 / self.polyphony # 同時発音数を固定とする 111 for i in range(len(self.wave)): 112 s = int(amp * sum(map(Oscillator.wave, self.oscillators.values()))) 113 wave[i] = s # max(-32767, min(s, 32767)) # clampしない 114 wave = struct.pack(self.pack_format, *wave) 115 return wave 116 117 def update_oscillators(self, keys): 118 """NOTE_ONのキー集合によりオシレーター群を更新する""" 119 120 # もはや発音しないオシレーターを削除 121 osc_exists = len(self.oscillators) > 0 122 self.oscillators = {osc.key: osc for osc in filter(Oscillator.is_playing, self.oscillators.values())} 123 if osc_exists and len(self.oscillators) == 0: 124 print("all oscillators removed") 125 126 # 新たにNOTE_ONとなったオシレーターを追加 127 new_keys = filter(lambda key: key not in self.oscillators, keys) 128 for key in new_keys: 129 max_level = keys[key] / 127 130 sustain_level = max_level * 0.9 131 attack_time = 0.3 132 decay_time = 0.1 133 release_time = 0.1 134 self.oscillators[key] = Oscillator(key, max_level, sustain_level, attack_time, decay_time, release_time) 135 print("note on", key) 136 137 # NOTE_OFFとなったオシレーターの状態を変更(リリース制御のため) 138 for osc_key in self.oscillators: 139 if osc_key not in keys: 140 if self.oscillators[osc_key].note_off(): 141 print("note off", osc_key) 142 143# 再生================================ 144 145 146def play(stream, data): # 再生用関数、ストリームと波形データを引数に 147 # チャンク単位でストリームに出力し音声を再生 148 chunk = 1048 149 sp = 0 # 再生位置ポインタ 150 buffer = data[sp:sp+chunk] 151 while buffer: 152 stream.write(buffer) 153 sp = sp + chunk 154 buffer = data[sp:sp+chunk] 155 156# ========================================== 157 158 159if __name__ == '__main__': 160 synthesiser = Synthesizer(10) 161 while True: 162 Key_input() # キーの取得 163 keys = {k[0]: k[1] for k in key_list} 164 wave = synthesiser.generate_wave(keys) 165 play(stream, wave) # 波形の再生 166 End() # このままでは決して呼び出されませんが...

投稿2018/02/16 05:49

編集2018/02/16 10:54
KSwordOfHaste

総合スコア18394

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/02/16 08:55

回答ありがとうございます。 鍵盤が押されてからの時間をカウントしたものを0.01~1.00の値に直し s += amp * np.sin(2 * np.pi * f * n / 44100)*freqList[item][1] この文の右辺に乗算して強引に大きさを変えてアタックを線形にしてみました。 音声はゆっくりと音量が大きくなり、一定の大きさでとまるようになりましたが サスティン中のノイズは全く無いのですが やはり音の最初でプツと途切れるようなノイズが発生してしまいます…
KSwordOfHaste

2018/02/16 09:36 編集

エンベロープをつけて同時撥音数を固定にすれば波形が不連続にならずにすみ雑音が消えると思ったので、自分でやってみたコードを追記しました。 しかし追記にも書いたのですが雑音が入るケースがあるようです。なにかもう少し考慮が必要なのかも知れません。中途半端なコメントになってしまい申し訳ないです。
KSwordOfHaste

2018/02/16 09:48

すみません、再度追記しました。PyCharmの「デバッグ実行」でなく「単なる実行」にしたら雑音でなくなりました。性能に左右される面もありそうで、質問者さんの環境でどうなるかやってみないとわからないですが。 自分の環境はWin10 64bit ARM 2core clock=3.8GHz メモリー8GBのわりと安っすいPCです。
退会済みユーザー

退会済みユーザー

2018/02/16 10:54

わざわざ、コードまでありがとうございます! こちらターミナルとエディタのみでの開発でしたので問題なく実行できました。 こちらのコードを参考に、自身のコードに適用していきたいと思います 当方pythonを触るのが初めてのため少々時間がかかってしまうと思いますが、よろしくお願いします!
KSwordOfHaste

2018/02/16 11:01

個人的に興味のある分野であることと、質問者さんの論理が分かり易かったのでちょっと自分でもやってみたくなりました。まずは雑音の解消を目指してみてほしいですが、うまくいったら倍音成分を加えるなんてのも楽しそうです。自分の非力なPCだと1~8までの倍音を追加すると性能が追い付かず雑音がでちゃいましたが・・・
退会済みユーザー

退会済みユーザー

2018/02/16 23:44

pygameのMIDI制御とあわせてしようすることができました。 ソース自体も分かりやすく色々と拡張できそうです、ありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問