Create a simple synthesiser – Part 2

Here's the second half our coding a simple synthesiser feature using a Raspberry Pi, Python and Cython.


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


            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 *


        # Position in wavetable

        self.position = 0.0

        # ADSR instance

        self.adsr = ADSR(samplerate)

        # Is this note done with = 0

    def __repr__(self):

        return (“Note: Frequency = {0}Hz, ”

                “Step Size = {1}”).format(self.freq,


    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:

   = 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]


            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): = 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.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”


    def stream_init(self): =

                    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 =‘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’)


    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.

An angled, isolated, studio lit shot of an electric keyboard.
An angled, isolated, studio lit shot of an electric keyboard.

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


    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


        return (self.playbuf.tostring(),


    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:




                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):


            # Wait to be notified to create more

            # samples



    def start(self):


        # Start synth loop thread

        t = threading.Thread(target=self.synth_loop)


    def freq_on(self, float freq):

        n = Note(self.wavetable, Synth.SAMPLERATE,


        print 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):



    def note_off(self, n):



    def toggle_note(self, n):

        if n in self.notes_on:

            print “note {0} off”.format(n)



            print “note {0} on”.format(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:

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 = []


    def getch():

        fd = sys.stdin.fileno()

        old_settings = termios.tcgetattr(fd)



            ch =


            termios.tcsetattr(fd, termios.TCSADRAIN,


        return ch

    def loop(self):

        while True:

            c = self.getch()

            if c == ‘q’:



            if c in self.keymap:

                n = self.keymap[c]


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:


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:


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.