-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmulti_synth.py
More file actions
executable file
·142 lines (115 loc) · 3.96 KB
/
Copy pathmulti_synth.py
File metadata and controls
executable file
·142 lines (115 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/env python3
# Polyphonic Python MIDI synthesizer
# python multi_synth.py [MIDI channel (0-15)]
import pyaudio
import mido
import sys, struct, math, time
try:
midi_channel = int(sys.argv[1])
except:
midi_channel = -1
# sleep time in main loop
SLEEP = 0.01
# audio buffer size (determines latency)
# Increase this if there is crackling audio output.
BSIZE = 256
# sample rate
ARATE = 44100
# maximum polyphony
MAXPOLY = 8
# volume
VOLUME = 3000
# sustain notes?
SUSTAIN = False
################################################################################
# list of currently active notes
notes = []
# callback function for audio data
def callback(in_data, frame_count, time_info, status):
data = b""
for i in range(frame_count):
v = 0
for n in notes:
v += n[2] * (
math.sin(n[0])
+ 0.5 * n[5] * math.sin(2 * n[0])
+ 0.25 * n[5] * math.sin(4 * n[0])
)
n[0] += 2 * math.pi / ARATE * n[1]
n[2] *= n[3]
b = struct.pack("h", round(VOLUME * v))
data += b
return data, pyaudio.paContinue
# open mido and pyaudio inputs/outputs
inport = mido.open_input()
paud = pyaudio.PyAudio()
stream = paud.open(
format=paud.get_format_from_width(2),
channels=1,
rate=ARATE,
output=True,
frames_per_buffer=BSIZE,
stream_callback=callback,
)
# print("latency [s] = %.5f" % stream.get_output_latency())
while True:
for msg in inport.iter_pending():
# check if note belongs to correct MIDI channel
if midi_channel > -1:
if msg.channel != midi_channel:
continue
# process new note
if msg.type == "note_on":
if msg.velocity == 0:
# turn note off (if velocity = 0)
if not SUSTAIN:
for n in notes:
if n[4] == msg.note:
n[3] = n[3] ** 6
else:
# get note frequency in Hz
freq = 440 * 2 ** ((msg.note - 69) / 12)
# get amplitude loss factor per sample
# (higher frequencies decay more quickly)
a_min, a_max, a_sel = math.log(21), math.log(108), math.log(msg.note)
lossfac = 50000 - 49000 * ((a_sel - a_min) / (a_max - a_min))
lossfac *= ARATE / 44100
amp_loss = 1 - 1 / lossfac
# get harmonic content factor (like PySynth A)
# - strong harmonics in lower octaves
# - no harmonics in higher octaves
lf_fac = (math.log(freq) - 3) / 4
if lf_fac > 1:
harm = 0
else:
harm = 2 * (1 - lf_fac)
# append new note to list of active notes
# note data:
# 0 * current oscillator phase
# 1 * frequency in Hz
# 2 * current amplitude
# 3 * amplitude loss factor
# 4 * MIDI key number
# 5 * harmonic content factor
notes.append([0, freq, 1, amp_loss, msg.note, harm])
# remove notes that have gone almost silent
newnotes = []
for n in notes:
if n[2] > 0.001:
newnotes.append(n)
notes = newnotes
# apply maximum polyphony cutoff with priority for latest notes
if len(notes) > MAXPOLY:
notes = notes[-MAXPOLY:]
# increase amplitude loss of note when note_off event happens
if msg.type == "note_off" and not SUSTAIN:
for n in notes:
if n[4] == msg.note:
n[3] = n[3] ** 6
try:
time.sleep(SLEEP)
except: # exception handler hides ugly backtrace when pressing Ctrl-C
break
stream.close()
paud.terminate()
inport.close()