回答編集履歴
5
日本語が下手すぎる点を訂正
answer
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
- 無音からいきなり最大振幅にすると(多分アンプの特性によって?)音割れする
|
4
4
|
- 最大振幅からいきなり振幅0にしても同じ
|
5
|
-
- 同時
|
5
|
+
- 同時発音数が変化した際にampを突然変化させると波に連続性がなくなるため音割れする
|
6
|
-
同時
|
6
|
+
同時発音数というのは和音を再生する際には結構変化するので、本式のシンセサイザー(あるいはミキサー)には波形を調整するコンプレッサーとかリミッターと呼ばれるものが備わっています。これはご質問のコードにあるように単に範囲外をclampするという方法では実現できません。そうやってしまうと波形がひずみ結局音割れします。
|
7
7
|
|
8
|
-
波形を直接生成するような場合、こうした音の特性に注意する必要があ
|
8
|
+
波形を直接生成するような場合、こうした音の特性に注意する必要があります。自然界の音は[エンベロープ](https://www.g200kg.com/jp/docs/dic/envelope.html)に従って振幅が徐々に変化するのが普通ですので、ソフトウェアで波形を生成する場合でも最小限のエンベロープ制御を行い波形が不連続にならないようにするとよいでしょう。
|
9
9
|
|
10
10
|
なお、基本体なエンベロープ制御は以下のような感じに考えるとよいと思います。
|
11
11
|
|
@@ -21,12 +21,12 @@
|
|
21
21
|
プログラムの設計的な面について考えると、一つ一つのNOTE(特定の音色の特定のピッチ)に対して独立したオシレーターインスタンスを用いることにして、NOTE_ONの瞬間に新たな初期状態のオシレーターを生成し、オシレーターが時間変化によって適切な振幅で波形を生成し、NOTE_OFFでリリースが始まり振幅が完全に0になったらオシレーターインスタンスを消去するといった方法論が考えられます。(自分はJavaの標準ライブラリーのシンセサイザーぐらいしか見たことないですが、それはこういう手法を取っていました)
|
22
22
|
そのようなオシレーターを複数保持しておき、一定時間ごと(ご質問にあるコードですと0.01秒ごと)にその時点で存在している全てのオシレーターの波形を合成して結果の波形を生成するイメージです。
|
23
23
|
|
24
|
-
なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、**滑らかに調整**というのがなかなかめんどくさいのです。そこで、とりあえずは同時
|
24
|
+
なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、**滑らかに調整**というのがなかなかめんどくさいのです。そこで、とりあえずは同時発音数を適当な値に固定してしまい(例えば8とか)、演奏する際にその限界を超えて発音するような事態を避けるのが簡単でしょう。同時発音数というのは同時にNOTE_ONとなっている数の最大数よりも大きくなる点に注意してください。NOTE_OFFしてもリリースタイムの期間の間振幅を徐々に小さくしながら音が鳴り続けるため「音が完全に消えるまでには若干時間がかかる」からです。
|
25
25
|
|
26
26
|
---
|
27
27
|
追記:実際に単純なエンベロープにしてやってみた例を挙げてみます。少々長いのでちょっと気が引けますが。自分の環境ではMIDI-INがすぐに使えないので、Key_inputの部分は自動演奏に置き換えています。質問者さんの環境ではこの関数を書き替えずに実行すれば演奏の通りに音が鳴らせると思います。(すみませんが、自動演奏に変えたため元のコードからpygame.midiの部分のコードは削除してあります)
|
28
28
|
|
29
|
-
下記コードでは0.4秒ごとにC4~B#4までの音を順に自動演奏していますが、その限りでは雑音はでないようでした。しかし・・・エンベロープを滑らかにしたつもりかつ同時
|
29
|
+
下記コードでは0.4秒ごとにC4~B#4までの音を順に自動演奏していますが、その限りでは雑音はでないようでした。しかし・・・エンベロープを滑らかにしたつもりかつ同時発音数10固定なんですが、自動演奏で和音にしたとたん演奏開始時点で雑音が・・・
|
30
30
|
|
31
31
|
単なるバグなのかも知れませんし、Pythonのスピードとか自分の配慮が不足している点が何かあるのかも知れません・・・
|
32
32
|
|
4
誤記訂正・コードコメント訂正
answer
CHANGED
@@ -21,7 +21,7 @@
|
|
21
21
|
プログラムの設計的な面について考えると、一つ一つのNOTE(特定の音色の特定のピッチ)に対して独立したオシレーターインスタンスを用いることにして、NOTE_ONの瞬間に新たな初期状態のオシレーターを生成し、オシレーターが時間変化によって適切な振幅で波形を生成し、NOTE_OFFでリリースが始まり振幅が完全に0になったらオシレーターインスタンスを消去するといった方法論が考えられます。(自分はJavaの標準ライブラリーのシンセサイザーぐらいしか見たことないですが、それはこういう手法を取っていました)
|
22
22
|
そのようなオシレーターを複数保持しておき、一定時間ごと(ご質問にあるコードですと0.01秒ごと)にその時点で存在している全てのオシレーターの波形を合成して結果の波形を生成するイメージです。
|
23
23
|
|
24
|
-
なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、**滑らかに調整**というのがなかなかめんどくさいのです。そこで、とりあえずは同時撥音数を適当な値に固定してしまい(例えば8とか)、演奏する際にその限界を超えて発音するような事態を避けるのが簡単と思います。同時撥音数というのは同時にNOTE_ONとなっている数の最大数よりも大きくなる点に注意してください。NOTE_OFFしてもリリースタイムの期間の間
|
24
|
+
なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、**滑らかに調整**というのがなかなかめんどくさいのです。そこで、とりあえずは同時撥音数を適当な値に固定してしまい(例えば8とか)、演奏する際にその限界を超えて発音するような事態を避けるのが簡単と思います。同時撥音数というのは同時にNOTE_ONとなっている数の最大数よりも大きくなる点に注意してください。NOTE_OFFしてもリリースタイムの期間の間振幅を徐々に小さくしながら音が鳴り続けるため「音が完全に消えるまでには若干時間がかかる」からです。
|
25
25
|
|
26
26
|
---
|
27
27
|
追記:実際に単純なエンベロープにしてやってみた例を挙げてみます。少々長いのでちょっと気が引けますが。自分の環境ではMIDI-INがすぐに使えないので、Key_inputの部分は自動演奏に置き換えています。質問者さんの環境ではこの関数を書き替えずに実行すれば演奏の通りに音が鳴らせると思います。(すみませんが、自動演奏に変えたため元のコードからpygame.midiの部分のコードは削除してあります)
|
@@ -68,7 +68,7 @@
|
|
68
68
|
key = 60 + int((t - 3) / 0.4) % 12
|
69
69
|
key_list = [[key, 110]]
|
70
70
|
# 以下のように、3度の和音にしたら同時発音数=10でも音割れした。(Win10 64bit)
|
71
|
-
#
|
71
|
+
# =>PyCharmのデバッグ実行では音割れしたが通常実行にすると音割れしなくなった
|
72
72
|
# ---------------------------------------------
|
73
73
|
# key_list = [[key, 120], [key + 4, 100]]
|
74
74
|
else:
|
3
追記2
answer
CHANGED
@@ -30,6 +30,9 @@
|
|
30
30
|
|
31
31
|
単なるバグなのかも知れませんし、Pythonのスピードとか自分の配慮が不足している点が何かあるのかも知れません・・・
|
32
32
|
|
33
|
+
追記2:PyCharm上で実行していたのですが、ふと思いついて「デバッグ実行」ではなく普通の「実行」にしたら和音にしても雑音でなくなりました!
|
34
|
+
想像ですが、PyCharmでデバッグ実行していたためインタープリタの実行スピードが音声再生においついてなかったのだと思います・・・
|
35
|
+
|
33
36
|
```Python
|
34
37
|
import math
|
35
38
|
import pyaudio
|
2
コード追記
answer
CHANGED
@@ -21,4 +21,180 @@
|
|
21
21
|
プログラムの設計的な面について考えると、一つ一つのNOTE(特定の音色の特定のピッチ)に対して独立したオシレーターインスタンスを用いることにして、NOTE_ONの瞬間に新たな初期状態のオシレーターを生成し、オシレーターが時間変化によって適切な振幅で波形を生成し、NOTE_OFFでリリースが始まり振幅が完全に0になったらオシレーターインスタンスを消去するといった方法論が考えられます。(自分はJavaの標準ライブラリーのシンセサイザーぐらいしか見たことないですが、それはこういう手法を取っていました)
|
22
22
|
そのようなオシレーターを複数保持しておき、一定時間ごと(ご質問にあるコードですと0.01秒ごと)にその時点で存在している全てのオシレーターの波形を合成して結果の波形を生成するイメージです。
|
23
23
|
|
24
|
-
なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、**滑らかに調整**というのがなかなかめんどくさいのです。そこで、とりあえずは同時撥音数を適当な値に固定してしまい(例えば8とか)、演奏する際にその限界を超えて発音するような事態を避けるのが簡単と思います。同時撥音数というのは同時にNOTE_ONとなっている数の最大数よりも大きくなる点に注意してください。NOTE_OFFしてもリリースタイムの期間の間神父が徐々に小さくなるため「音が完全に消えるまでには若干時間がかかる」からです。
|
24
|
+
なお、前述したコンプレッサーとかリミッターをソフトウェア的に実現するのは少々面倒だと思います。振幅がオーバーフローしそうになったときだけ滑らかに波形を調整しないといけないのですが、**滑らかに調整**というのがなかなかめんどくさいのです。そこで、とりあえずは同時撥音数を適当な値に固定してしまい(例えば8とか)、演奏する際にその限界を超えて発音するような事態を避けるのが簡単と思います。同時撥音数というのは同時にNOTE_ONとなっている数の最大数よりも大きくなる点に注意してください。NOTE_OFFしてもリリースタイムの期間の間神父が徐々に小さくなるため「音が完全に消えるまでには若干時間がかかる」からです。
|
25
|
+
|
26
|
+
---
|
27
|
+
追記:実際に単純なエンベロープにしてやってみた例を挙げてみます。少々長いのでちょっと気が引けますが。自分の環境ではMIDI-INがすぐに使えないので、Key_inputの部分は自動演奏に置き換えています。質問者さんの環境ではこの関数を書き替えずに実行すれば演奏の通りに音が鳴らせると思います。(すみませんが、自動演奏に変えたため元のコードからpygame.midiの部分のコードは削除してあります)
|
28
|
+
|
29
|
+
下記コードでは0.4秒ごとにC4~B#4までの音を順に自動演奏していますが、その限りでは雑音はでないようでした。しかし・・・エンベロープを滑らかにしたつもりかつ同時撥音数10固定なんですが、自動演奏で和音にしたとたん演奏開始時点で雑音が・・・
|
30
|
+
|
31
|
+
単なるバグなのかも知れませんし、Pythonのスピードとか自分の配慮が不足している点が何かあるのかも知れません・・・
|
32
|
+
|
33
|
+
```Python
|
34
|
+
import math
|
35
|
+
import pyaudio
|
36
|
+
import struct
|
37
|
+
import time
|
38
|
+
|
39
|
+
# PyAudio制御関連=============================
|
40
|
+
SAMPLE_RATE = 44100
|
41
|
+
|
42
|
+
if __name__ == '__main__':
|
43
|
+
p = pyaudio.PyAudio()
|
44
|
+
# ストリームを開く
|
45
|
+
stream = p.open(format=pyaudio.paInt16, channels=1, rate=SAMPLE_RATE, output=1, stream_callback=None)
|
46
|
+
|
47
|
+
|
48
|
+
def End(): # 終了処理(必ず入れる)
|
49
|
+
stream.close()
|
50
|
+
p.terminate()
|
51
|
+
|
52
|
+
|
53
|
+
# Key管理================================
|
54
|
+
|
55
|
+
key_list = [] # 押されてるキーを保持するリスト
|
56
|
+
start_time = time.time() # MIDI-INなしで自動演奏するための小細工
|
57
|
+
|
58
|
+
|
59
|
+
def Key_input():
|
60
|
+
# MIDI-INなしで自動演奏するための小細工
|
61
|
+
# MIDI-INが使えるならオリジナルのコードそのままでよいはず・・・
|
62
|
+
global key_list
|
63
|
+
t = (time.time() - start_time)
|
64
|
+
if 3 < t < 20:
|
65
|
+
key = 60 + int((t - 3) / 0.4) % 12
|
66
|
+
key_list = [[key, 110]]
|
67
|
+
# 以下のように、3度の和音にしたら同時発音数=10でも音割れした。(Win10 64bit)
|
68
|
+
# なぜそうなるか分かりませんでした... ><
|
69
|
+
# ---------------------------------------------
|
70
|
+
# key_list = [[key, 120], [key + 4, 100]]
|
71
|
+
else:
|
72
|
+
key_list = []
|
73
|
+
return
|
74
|
+
|
75
|
+
|
76
|
+
# 周波数とか作るところ=======================
|
77
|
+
|
78
|
+
|
79
|
+
class Oscillator:
|
80
|
+
def __init__(self, key, max_level, sustain_level, attack_time, decay_time, release_time):
|
81
|
+
self.key = key
|
82
|
+
freq = 440.0 * (2 ** ((key - 69) / 12))
|
83
|
+
self.dt = math.pi * 2 * freq / SAMPLE_RATE
|
84
|
+
self.max = max_level
|
85
|
+
self.sustain = sustain_level
|
86
|
+
self.attack_t = int(attack_time * SAMPLE_RATE)
|
87
|
+
self.decay_t = int(decay_time * SAMPLE_RATE)
|
88
|
+
self.release_t = int(release_time * SAMPLE_RATE)
|
89
|
+
self.released = None
|
90
|
+
self.time = 0
|
91
|
+
self.amp = 0
|
92
|
+
|
93
|
+
def calc_amp(self):
|
94
|
+
if self.released is None:
|
95
|
+
# まだNOTE_ON状態
|
96
|
+
if self.time < self.attack_t:
|
97
|
+
return Oscillator.interpolate(self.time, self.attack_t, 0.0, self.max)
|
98
|
+
else:
|
99
|
+
return Oscillator.interpolate(self.time - self.attack_t, self.decay_t, self.max, self.sustain)
|
100
|
+
else:
|
101
|
+
# NOTE_OFF状態
|
102
|
+
return Oscillator.interpolate(self.time - self.released, self.release_t, self.sustain, 0.0)
|
103
|
+
|
104
|
+
# 振幅の直線補間用
|
105
|
+
@staticmethod
|
106
|
+
def interpolate(cur_t, max_t, start_level, end_level):
|
107
|
+
t = cur_t / max_t if cur_t < max_t else 1
|
108
|
+
return start_level * (1 - t) + end_level * t
|
109
|
+
|
110
|
+
def wave(self):
|
111
|
+
self.amp = self.calc_amp()
|
112
|
+
w = self.amp * math.sin(self.time * self.dt)
|
113
|
+
self.time += 1
|
114
|
+
return w
|
115
|
+
|
116
|
+
def note_off(self):
|
117
|
+
if self.released is None:
|
118
|
+
self.sustain = self.amp # attack/decay中にNOTE_OFFされた場合への配慮
|
119
|
+
self.released = self.time
|
120
|
+
return True
|
121
|
+
else:
|
122
|
+
return False
|
123
|
+
|
124
|
+
def is_playing(self):
|
125
|
+
return self.released is None or self.time < self.released + self.release_t
|
126
|
+
|
127
|
+
|
128
|
+
class Synthesizer:
|
129
|
+
def __init__(self, polyphony=8):
|
130
|
+
self.oscillators = {}
|
131
|
+
self.polyphony = polyphony
|
132
|
+
buffer_size = 1048 // 2 # 44100 1sec 4410 0.1sec 441 0.01sec
|
133
|
+
latency = buffer_size / SAMPLE_RATE
|
134
|
+
self.wave = [0] * int(latency * SAMPLE_RATE)
|
135
|
+
self.pack_format = 'h' * len(self.wave)
|
136
|
+
print("polyphony=", self.polyphony, "latency=", latency)
|
137
|
+
|
138
|
+
def generate_wave(self, keys):
|
139
|
+
"""実際の波形を生成"""
|
140
|
+
self.update_oscillators(keys)
|
141
|
+
wave = self.wave
|
142
|
+
# print(keys)
|
143
|
+
amp = 32767 / self.polyphony # 同時発音数を固定とする
|
144
|
+
for i in range(len(self.wave)):
|
145
|
+
s = int(amp * sum(map(Oscillator.wave, self.oscillators.values())))
|
146
|
+
wave[i] = s # max(-32767, min(s, 32767)) # clampしない
|
147
|
+
wave = struct.pack(self.pack_format, *wave)
|
148
|
+
return wave
|
149
|
+
|
150
|
+
def update_oscillators(self, keys):
|
151
|
+
"""NOTE_ONのキー集合によりオシレーター群を更新する"""
|
152
|
+
|
153
|
+
# もはや発音しないオシレーターを削除
|
154
|
+
osc_exists = len(self.oscillators) > 0
|
155
|
+
self.oscillators = {osc.key: osc for osc in filter(Oscillator.is_playing, self.oscillators.values())}
|
156
|
+
if osc_exists and len(self.oscillators) == 0:
|
157
|
+
print("all oscillators removed")
|
158
|
+
|
159
|
+
# 新たにNOTE_ONとなったオシレーターを追加
|
160
|
+
new_keys = filter(lambda key: key not in self.oscillators, keys)
|
161
|
+
for key in new_keys:
|
162
|
+
max_level = keys[key] / 127
|
163
|
+
sustain_level = max_level * 0.9
|
164
|
+
attack_time = 0.3
|
165
|
+
decay_time = 0.1
|
166
|
+
release_time = 0.1
|
167
|
+
self.oscillators[key] = Oscillator(key, max_level, sustain_level, attack_time, decay_time, release_time)
|
168
|
+
print("note on", key)
|
169
|
+
|
170
|
+
# NOTE_OFFとなったオシレーターの状態を変更(リリース制御のため)
|
171
|
+
for osc_key in self.oscillators:
|
172
|
+
if osc_key not in keys:
|
173
|
+
if self.oscillators[osc_key].note_off():
|
174
|
+
print("note off", osc_key)
|
175
|
+
|
176
|
+
# 再生================================
|
177
|
+
|
178
|
+
|
179
|
+
def play(stream, data): # 再生用関数、ストリームと波形データを引数に
|
180
|
+
# チャンク単位でストリームに出力し音声を再生
|
181
|
+
chunk = 1048
|
182
|
+
sp = 0 # 再生位置ポインタ
|
183
|
+
buffer = data[sp:sp+chunk]
|
184
|
+
while buffer:
|
185
|
+
stream.write(buffer)
|
186
|
+
sp = sp + chunk
|
187
|
+
buffer = data[sp:sp+chunk]
|
188
|
+
|
189
|
+
# ==========================================
|
190
|
+
|
191
|
+
|
192
|
+
if __name__ == '__main__':
|
193
|
+
synthesiser = Synthesizer(10)
|
194
|
+
while True:
|
195
|
+
Key_input() # キーの取得
|
196
|
+
keys = {k[0]: k[1] for k in key_list}
|
197
|
+
wave = synthesiser.generate_wave(keys)
|
198
|
+
play(stream, wave) # 波形の再生
|
199
|
+
End() # このままでは決して呼び出されませんが...
|
200
|
+
```
|
1
文章不備訂正
answer
CHANGED
@@ -5,9 +5,9 @@
|
|
5
5
|
- 同時撥音数が変化した際にampを突然変化させると波に連続性がなくなるため音割れする
|
6
6
|
同時撥音数というのは和音を再生する際には結構変化するので、本式のシンセサイザー(あるいはミキサー)には波形を調整するコンプレッサーとかリミッターと呼ばれるものが備わっていると思います。これはご質問のコードにあるように単に範囲外をclampするという方法では実現できません。そういう方式では波形がひずむため結局音割れします。
|
7
7
|
|
8
|
-
こうした音の特性に注意する必要がある
|
8
|
+
波形を直接生成するような場合、こうした音の特性に注意する必要があると思います。自然界の音は[エンベロープ](https://www.g200kg.com/jp/docs/dic/envelope.html)に従って振幅が徐々に変化するのが普通です。MIDIのシンセサイザーにはピンキリがあると思いますが、どんな単純なものでも最小限のエンベロープ制御を行っており波形が不連続にならないように(音割れしないように)なっていると思います。
|
9
9
|
|
10
|
-
基本体なエンベロープ
|
10
|
+
なお、基本体なエンベロープ制御は以下のような感じに考えるとよいと思います。
|
11
11
|
|
12
12
|
アタック:振幅0から最大までになる時間
|
13
13
|
ディケイ:振幅最大から定常状態の振幅(最大よりちょっと小さい)まで下がる時間
|