If you do Linux and audio seriously, you will already have found out about JACK. If you know how to program and you like fast development, you might have already fallen in love with Python. This tutorial aims at combining the two worlds. Let us write a simple JACK client in Python which connects to system output and produces an audible sine wave.
I’ll assume you have the JACK server installed, set up and running as well as a version of Python 2 installed. You will also need to install the Python bindings to JACK, which you’ll get here. Unfortunately, they aren’t yet available for Python 3, and I encourage you to bug the developer team to port it or to port it yourself. (Also, the included examples are – in my opinion – very old fashioned and/or poorly written.)
The numpy library is also needed.
Let’s start. Fire up an interactive console and get going:
>>> import jack
>>> my_test_client = jack.Client("my_test_client")
So far, we have imported the jack bindings and created an unactivated client called my_test_client. Before anything happens, we have to activate it, which means it will start to talk to the JACK server.
>>> my_test_client.activate()
If you get errors here, then it’s because the JACK server is not running properly.
If everything went well, JACK’s log will tell you that there is a New client 'my_test_client' with PID ###.
The new client can’t receive or send sound yet because it lacks ports. We’ll have to grant them to it, and specify whether they’re input or output ports:
>>> my_test_client.register_port("in", jack.IsInput)
>>> my_test_client.register_port("out", jack.IsOutput)
>>> my_out_port_name = my_test_client.get_client_name() + ":out"
>>> my_out_port_name
'my_test_client:out'
>>> for system_playback_port_number in (1, 2):
... my_test_client.connect(my_out_port_name, "system:playback_{}".format(system_playback_port_number))
If you’re using a GUI tool to manage JACK like QJackCtl, you will notice that my_test_client will now have shown up in the respective lists of both input and output clients and the out port is connected to both system playback ports.
Let us now generate the sound we want to make. JACK works sample based, so we need to generate samples, for which in turn we need to know the sample rate.
>>> sample_rate = my_test_client.get_sample_rate()
>>> sample_rate
48000
Your value may differ slightly.
The samples are numpy arrays. Let us create the sample we will output:
>>> seconds_to_play = 3
>>> number_of_samples = seconds_to_play * sample_rate # Make sure to int this if seconds_to_play is not an int
>>> import numpy
>>> time_index = numpy.linspace(0, seconds_to_play, number_of_samples)
>>> frequency = 440
>>> _output_samples = numpy.sin(2*numpy.pi * frequency * time_index)
_output_samples is not yet in the correct format.
First of all, PyJack internally needs to make sure that the samples are of type float. (_output_samples.astype("f"))
Second, we need to change the shape of the sample array for the following reason: In principle, output and input can be many more channels than 1. It could be stereo (2 channels) or even some Surround system (5, 7 or more channels). PyJack needs the samples in a two-dimensional array where the first index counts the channels and the second counts the samples. Since we’re happy to output mono, we only need the 1 channel. (numpy.reshape with shape (1, number_of_samples))
>>> output_samples = numpy.reshape(_output_samples.astype("f"), (1, number_of_samples))
We will also need to provide an input array (of appropriate shape and type) where the JACK callback can store the incoming data. We will conveniently make it the size of the JACK buffer size since every JACK process run we’ll exchange just that amount of data.
>>> buffer_size = my_test_client.get_buffer_size()
>>> input_samples = numpy.zeros((1,buffer_size), 'f')
In this example, the input_samples array really is just a dummy. If we were recording sound, we might actually use the values it contains.
We are now ready to send the data to the JACK Server. JACK will only exchange data chunks of the size of the buffer at a time, so we need to take slices of our output_samples. The complete slice operator will be [:,i:i+buffer_size], where the first colon is for copying all channels and the second slice is one particular slice of length buffer_size.
i ranges over the complete number_of_samples in steps of buffer_size where care is taken that i + buffer_size is still inside the output_samples.
>>> for i in range(0, number_of_samples - buffer_size, buffer_size):
>>> output_sample_chunk = output_samples[:,i:i+buffer_size]
>>> my_test_client.process(output_sample_chunk, input_samples)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
my_test_client.process(output_sample_chunk, input_samples)
jack.InputSyncError: Input data stream is not synchronized.
Oops, something went horribly wrong. Or did it? Remember that JACK is a low-latency sound server and that it will not forgive you for offering your buffer-sized chunks of samples too late. But at least our first buffer swap will not have a chance of coming in time since we waited seconds or minutes from activating and connecting the client until sending the samples because we are in interactive mode. That’s much longer than the length of one buffer, which will be
or even much shorter on a lot of systems.
“No problem”, you’ll say, “let’s just put all the code in a script, run it and the error will disappear.” Unfortunately not: There are so many functions we called that running them all directly after each other that they already take longer than one buffer swap. A typical candidate for such a thing is the calculation of the output buffer, which will certainly take several milliseconds for every second of generated sample. In a good program, such tasks have to be done before the client connects to the server or ideally even in a separate thread.
But even disregarding the fact that we might have to face an input synchronisation error at the beginning of our sending the samples, we should be prepared for such errors just in the middle as well, as the system might become busy and the Python interpreter doesn’t get enough processor time to offer the buffers. In such situations, playback should not just interrupt, but display an error message and carry on immediately. We’ll do that by just catching the exception:
for i in range(0, number_of_samples - buffer_size, buffer_size):
>>> print("durchlauf an Stelle {0}".format(i), clock.tick())
>>> try:
>>> output_sample_chunk = output_samples[:,i:i+buffer_size]
>>> my_test_client.process(output_sample_chunk, input_samples)
>>> except jack.InputSyncError:
>>> print("InputSyncError")
InputSyncError
InputSyncError
And that’s it! You should already hear a nice “a” from both speakers now.
And in order to be kind to the JACK server, we will neatly disconnect:
>>> my_test_client.deactivate()
And the JACK log will show something like:
Disconnecting 'my_test_client:out' from 'system:playback_1'
Disconnecting 'my_test_client:out' from 'system:playback_2'
Client 'my_test_client' with PID ### is out
For further reading, check out the examples that come with PyJack.
If you can’t get rid of synchronisation errors, make ure you have realtime privileges on your system and run python in realtime, for example like this: chrt 20 python ...