Skip to content

Commit 51e8eac

Browse files
committed
Add MIDI-Sine-NumPy example
1 parent 1e48dae commit 51e8eac

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

doc/examples.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,10 @@ Simple MIDI Synth
4444
:download:`midi_sine.py <../examples/midi_sine.py>`
4545

4646
.. literalinclude:: ../examples/midi_sine.py
47+
48+
Simple MIDI Synth (NumPy Edition)
49+
---------------------------------
50+
51+
:download:`midi_sine_numpy.py <../examples/midi_sine_numpy.py>`
52+
53+
.. literalinclude:: ../examples/midi_sine_numpy.py

examples/midi_sine_numpy.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python3
2+
"""Very basic MIDI synthesizer.
3+
4+
This does the same as midi_sine.py, but it uses NumPy and block
5+
processing. It is therefore much more efficient. But there are still
6+
many allocations and dynamically growing and shrinking data structures.
7+
8+
"""
9+
import jack
10+
import numpy as np
11+
import threading
12+
13+
# First 4 bits of status byte:
14+
NOTEON = 0x9
15+
NOTEOFF = 0x8
16+
17+
attack_seconds = 0.01
18+
release_seconds = 0.2
19+
20+
attack = None
21+
release = None
22+
fs = None
23+
voices = {}
24+
25+
client = jack.Client('MIDI-Sine-NumPy')
26+
midiport = client.midi_inports.register('midi_in')
27+
audioport = client.outports.register('audio_out')
28+
event = threading.Event()
29+
30+
31+
def m2f(note):
32+
"""Convert MIDI note number to frequency in Hertz.
33+
34+
See https://en.wikipedia.org/wiki/MIDI_Tuning_Standard.
35+
36+
"""
37+
return 2 ** ((note - 69) / 12) * 440
38+
39+
40+
def update_envelope(envelope, begin, target, vel):
41+
"""Helper function to calculate envelopes.
42+
43+
envelope: array of velocities, will be mutated
44+
begin: sample index where ramp begins
45+
target: sample index where *vel* shall be reached
46+
vel: final velocity value
47+
48+
If the ramp goes beyond the blocksize, it is supposed to be
49+
continued in the next block.
50+
51+
A reference to *envelope* is returned, as well as the (unchanged)
52+
*vel* and the target index of the following block where *vel* shall
53+
be reached.
54+
55+
"""
56+
blocksize = len(envelope)
57+
old_vel = envelope[begin]
58+
slope = (vel - old_vel) / (target - begin + 1)
59+
ramp = np.arange(min(target, blocksize) - begin) + 1
60+
envelope[begin:target] = ramp * slope + old_vel
61+
if target < blocksize:
62+
envelope[target:] = vel
63+
target = 0
64+
else:
65+
target -= blocksize
66+
return envelope, vel, target
67+
68+
69+
@client.set_process_callback
70+
def process(blocksize):
71+
"""Main callback."""
72+
73+
# Step 1: Update/delete existing voices from previous block
74+
75+
# Iterating over a copy because items may be deleted:
76+
for pitch in list(voices):
77+
envelope, vel, target = voices[pitch]
78+
if any([vel, target]):
79+
envelope[0] = envelope[-1]
80+
voices[pitch] = update_envelope(envelope, 0, target, vel)
81+
else:
82+
del voices[pitch]
83+
84+
# Step 2: Create envelopes from the MIDI events of the current block
85+
86+
for offset, data in midiport.incoming_midi_events():
87+
if len(data) == 3:
88+
status, pitch, vel = bytes(data)
89+
# MIDI channel number is ignored!
90+
status >>= 4
91+
if status == NOTEON and vel > 0:
92+
try:
93+
envelope, _, _ = voices[pitch]
94+
except KeyError:
95+
envelope = np.zeros(blocksize)
96+
voices[pitch] = update_envelope(
97+
envelope, offset, offset + attack, vel)
98+
elif status in (NOTEON, NOTEOFF):
99+
# NoteOff velocity is ignored!
100+
try:
101+
envelope, _, _ = voices[pitch]
102+
except KeyError:
103+
print('NoteOff without NoteOn (ignored)')
104+
continue
105+
voices[pitch] = update_envelope(
106+
envelope, offset, offset + release, 0)
107+
else:
108+
pass # ignore
109+
else:
110+
pass # ignore
111+
112+
# Step 3: Create sine tones, apply envelopes, add to output buffer
113+
114+
buf = audioport.get_array()
115+
buf.fill(0)
116+
for pitch, (envelope, _, _) in voices.items():
117+
t = (np.arange(blocksize) + client.last_frame_time) / fs
118+
tone = np.sin(2 * np.pi * m2f(pitch) * t)
119+
buf += tone * envelope / 127
120+
121+
122+
@client.set_samplerate_callback
123+
def samplerate(samplerate):
124+
global fs, attack, release
125+
fs = samplerate
126+
attack = int(attack_seconds * fs)
127+
release = int(release_seconds * fs)
128+
voices.clear()
129+
130+
131+
@client.set_shutdown_callback
132+
def shutdown(status, reason):
133+
print('JACK shutdown:', reason, status)
134+
event.set()
135+
136+
with client:
137+
print('Press Ctrl+C to stop')
138+
try:
139+
event.wait()
140+
except KeyboardInterrupt:
141+
print('\nInterrupted by user')

0 commit comments

Comments
 (0)