
8. Attack, Decay, Sustain, Release
The ADSR class applies a volume curve over time to the raw output of an oscillator. It does this by returning a multiplier to the note that is a multiple between 0.0 and 1.0. The version we provide has an attack time of 100 ms, a decay time of 300 ms and a release time of 50 ms. You can try changing these values to see how it affects the sound.
The ADSR class does a lot of maths (44,100 times per second, per note). As such, we want to give types to all of the variables so that the maths can be optimised into a raw C loop where possible, because Python has a massive amount of overhead compared to C. This is what the cdef keyword does. If cdef public is used, then the variable can also be accessed from inside Python as well.
Full code for step 8
cdef class ADSR:
cdef float attack, decay, sustain_amplitude
cdef float release, multiplier
cdef public char state
cdef int samples_per_ms, samples_gone
def __init__(self, sample_rate):
self.attack = 1.0/100
self.decay = 1.0/300
self.sustain_amplitude = 0.7
self.release = 1.0/50
self.state = ‘A’
self.multiplier = 0.0
self.samples_per_ms = int(sample_rate / 1000)
self.samples_gone = 0
def next_val(self):
self.samples_gone += 1
if self.samples_gone > self.samples_per_ms:
self.samples_gone = 0
else:
return self.multiplier
if self.state == ‘A’:
self.multiplier += self.attack
if self.multiplier >= 1:
self.state = ‘D’
elif self.state == ‘D’:
self.multiplier -= self.decay
if self.multiplier <= self.sustain_amplitude:
self.state = ‘S’
elif self.state == ‘R’:
self.multiplier -= self.release
return self.multiplier
9. Generate notes
The note class is the core of our synthesiser. It uses the wavetable to generate waves of a specific frequency. The synthesiser asks the note class for a sample. After generating a sample, the ADSR multiplier is applied and then returned to the synthesiser. The maths of this are explained in the synthesis theory boxout on the opposite page.
The note class does as much maths as the ADSR class, so it is optimised as much as possible using cdef keywords. The cpdef keyword used for the next_sample function means that the function can be called from a non-cdef class. However, the main synth class is much too complicated to give static types to absolutely everything.

Full code for step 9
cdef class Note:
cdef int wavetable_len
cdef float position, step_size
cdef c_array.array wavetable
cdef public float freq
cdef public object adsr
cdef public int off
def __init__(self, wavetable, samplerate, freq):
# Reference to the wavetable we’re using
self.wavetable = wavetable
self.wavetable_len = len(wavetable)
# Frequency in Hz
self.freq = freq
# The size we need to step though the wavetable
# at each sample to get the desired frequency.
self.step_size = self.wavetable_len *
(freq/float(samplerate))
# Position in wavetable
self.position = 0.0
# ADSR instance
self.adsr = ADSR(samplerate)
# Is this note done with
self.off = 0
def __repr__(self):
return (“Note: Frequency = {0}Hz, ”
“Step Size = {1}”).format(self.freq,
self.step_size)
cpdef int next_sample(self):
# Do the next sample
cdef int pos_int, p1, p2, interpolated
cdef int out_sample = 0
cdef float pos_dec
cdef float adsr
adsr = self.adsr.next_val()
# Need to turn the note off
# synth will remove on next sample
if adsr < 0:
self.off = 1
return out_sample
pos_int = int(self.position)
pos_dec = self.position – pos_int
# Do linear interpolation
p1 = self.wavetable[pos_int]
p2 = 0
# Wrap around if the first position is at the
# end of the table
if pos_int + 1 == self.wavetable_len:
p2 = self.wavetable[0]
else:
p2 = self.wavetable[pos_int+1]
# Inerpolate between p1 and p2
interpolated = int(p1 + ((p2 – p1) * pos_dec))
out_sample += int(interpolated * adsr)
# Increment step size and wrap around if we’ve
# gone over the end of the table
self.position += self.step_size
if self.position >= self.wavetable_len:
self.position -= self.wavetable_len
return out_sample
class Synth:
BUFSIZE = 1024
SAMPLERATE = 44100
def __init__(self):
self.audio = pyaudio.PyAudio()
# Create output buffers
self.buf_a = array(‘h’, [0] * Synth.BUFSIZE)
self.buf_b = array(‘h’, [0] * Synth.BUFSIZE)
# Oldbuf and curbuf are references to buf_a or
# buf_b, not copies. We’re filling newbuf
# while playbuf is playing
self.playbuf = self.buf_b
self.newbuf = self.buf_a
self.load_wavetable()
self.notes = []
self.notes_on = []
# The synth loop will run in a separate thread.
# We will use this condition to notify it when
# we need more samples
self.more_samples = threading.Event()
self.exit = threading.Event()
# MIDI table of notes -> frequencies
self.midi_table = MIDITable()
def stop(self):
print “Exiting”
self.exit.set()
self.stream.stop_stream()
self.stream.close()
def stream_init(self):
self.stream = self.audio.open(
format = pyaudio.paInt16,
channels = 1,
rate = Synth.SAMPLERATE,
output = True,
frames_per_buffer = Synth.BUFSIZE,
stream_callback = self.callback)
def load_wavetable(self):
# Load wavetable and assert it is the
# correct format
fh = wave.open(‘square.wav’, ‘r’)
assert fh.getnchannels() == 1
assert fh.getframerate() == Synth.SAMPLERATE
assert fh.getsampwidth() == 2 # aka 16 bit
# Read the wavedata as a byte string. Then
# need to convert this into a sample array we
# can access with indexes
data = fh.readframes(fh.getnframes())
# h is a signed short aka int16_t
self.wavetable = array(‘h’)
self.wavetable.fromstring(data)
def swap_buffers(self):
tmp = self.playbuf
self.playbuf = self.newbuf
self.newbuf = tmp
# Setting the condition makes the synth loop
10. The audio flow
This synth class is the main class of the application. It has two sample buffers that are the length of the buffer size. While one buffer is being played by the sound card, the other buffer is being filled in a different thread. Once the sound card has played a buffer, the callback function is called. References to the buffers are swapped and the buffer that has just been filled is returned to the audio library.
The smaller the buffer size, the lower the latency. The Raspbian image isn’t optimised for real time audio by default so you may have trouble getting small buffer sizes. It also depends on the USB sound card used.

11. Synth loop
The start method of the synth class initialises the audio hardware and then starts the synth_loop method in its own thread. While the exit event is set to false, the do_sample function is called.
The do_sample function loops through the notes that are currently turned on and asks for a sample from each one. These samples are shifted right by three (ie divided by 2^3) and added to out_sample. The division ensures that the output sample can’t overflow (this is a very primitive method of adding notes together, but it works nonetheless).
The resulting sample is then put in the sample buffer. Once the buffer is full, the more_samples condition is cleared and the synth_loop thread waits to be notified that the buffer it has just built has been sent to the audio card. At this point, the synth can fill up the buffer that has just finished playing and the cycle continues.
Full code for step 11
# generate more samples
self.more_samples.set()
def callback(self, in_data, frame_count,
time_info, status):
# Audio card needs more samples so swap the
# buffers so we generate more samples and play
# back the play buffer we’ve just been filling
self.swap_buffers()
return (self.playbuf.tostring(),
pyaudio.paContinue)
def do_sample(self, int i):
cdef int out_sample = 0
# Go through each note and let it add to the
# overall sample
for note in self.notes:
if note.off:
self.notes.remove(note)
else:
out_sample += note.next_sample() >> 3
self.newbuf[i] = out_sample
def synth_loop(self):
cdef int i
12. Turn on notes
There are both note_on/off and freq_on/off functions that enable either MIDI notes or arbitrary frequencies to be turned on easily. Added to this, there is also a toggle note function which keeps track of MIDI notes that are on and turns them off if they are already on. The toggle note method is used specifically for keyboard input.
# For each sample we need to generate
for i in range(0, Synth.BUFSIZE):
self.do_sample(i)
# Wait to be notified to create more
# samples
self.more_samples.clear()
self.more_samples.wait()
def start(self):
self.stream_init()
# Start synth loop thread
t = threading.Thread(target=self.synth_loop)
t.start()
def freq_on(self, float freq):
n = Note(self.wavetable, Synth.SAMPLERATE,
freq)
print n
self.notes.append(n)
def freq_off(self, float freq):
# Set the ADSR state to release
for n in self.notes:
if n.freq == freq:
n.adsr.state = ord(‘R’)
def note_on(self, n):
self.freq_on(self.midi_table.get_note(n))
self.notes_on.append(n)
def note_off(self, n):
self.freq_off(self.midi_table.get_note(n))
self.notes_on.remove(n)
def toggle_note(self, n):
if n in self.notes_on:
print “note {0} off”.format(n)
self.note_off(n)
else:
print “note {0} on”.format(n)
self.note_on(n)
13. Add keyboard input
For keyboard input, we needed the ability to get a single character press from the screen. Python’s usual input code needs entering before returning to the program. Our code for this is inspired by: https://code.activestate.com/recipes/577977-get-single-keypress.
There is a mapping of letters on a keyboard to MIDI note numbers for an entire keyboard octave. We have tried to match the letter spacing to how a piano is laid out to make things easier. However, more innovative methods of input are left as an exercise to the reader.
class KBInput:
def __init__(self, synth):
self.synth = synth
self.keymap = {‘a’ : 60, ‘w’ : 61, ‘s’ : 62,
‘e’ : 63, ‘d’ : 64, ‘f’ : 65,
‘t’ : 66, ‘g’ : 67, ‘y’ : 68,
‘h’ : 69, ‘u’ : 70, ‘j’ : 71,
‘k’: 72}
self.notes_on = []
@staticmethod
def getch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN,
old_settings)
return ch
def loop(self):
while True:
c = self.getch()
if c == ‘q’:
self.synth.stop()
return
if c in self.keymap:
n = self.keymap[c]
self.synth.toggle_note(n)
14. Put it all together
The main function of the program creates an instance of the synth class and then starts the audio stream and synth loop thread. The start function will then return control to the main thread again.
At this point we create an instance of the KB input class and enter a loop that gets characters and toggles the corresponding MIDI note on or off. If the user presses the Q key, that will stop the synth and end the input loop. The program will then exit.
15. Compile the code
Exit your editor and run the compile script by typing
the following command:
./compile.sh
This may take around 30 seconds, so don’t worry if it isn’t instant. Once the compilation has finished, execute the synth.bin command using:
./synth.bin
Pressing keys from A all the way up to K on the keyboard will emulate the white keys on the piano. If you press a key again the note will go off successfully.