Jack lets you interconnect audio programs in easy way. If you don't know what it is or how to use it from user perspective, look at Jack user howto.
This howto shows you how to start programming using jack. Knowledge of C programming, command line and basic audio theory is required.
Some distributions install all necessary developer files along with program package (eg. Arch). Other may require you to install developer package for jack to get header files, pkg-conf files etc. For Ubuntu and Mint this package is named libjack-dev.
Table of Contents
In the first program you will:
You will learn how to:
Program source:
/* Connect to jack and tell sample rate. Compile with: cc -o jack-dev-1 jack-dev-1.c `pkg-config --cflags --libs jack` */ #include <stdio.h> #include <jack/jack.h> int main(void) { // connect to jack server jack_client_t *client = jack_client_open( "jack-dev-1", // name under which this client will be visible JackNoStartServer, // options NULL); // status information, NULL = not using if(client == NULL) { fprintf(stderr, "Connecting to jack failed\n"); return 1; } // display sample rate jack_nframes_t sample_rate = jack_get_sample_rate(client); printf("sample rate is %u\n", sample_rate); // disconnect from jack server jack_client_close(client); return 0; }
Program is very simple and self explanatory, so there isn't much to said.
By default jack will be started when program is trying to connect to it, and it is not running.
In this program we prevent this behaviour by using JackNoStartServer
option. If you wish to use
default options and make jack start when client tries to connect use JackNullOption
option
instead.
You can get additional info what went wrong if connecting to jack failed by using status information -
third parameter of jack_client_open()
.
Additional information is available at
jack docs of jack_client_open()
.
To include neccessary compilation flags and libraries for jack we are using pkg-config
program which handles this automatically. It just prints additional parameters that are to be added
to the compilation command - noting fancy, but very handy.
In the second program you will:
You will learn how to:
Program source:
/* Copy samples from input port to output port raising amplitude 2 times (making sound louder). Compile with: cc -o jack-dev-2 jack-dev-2.c `pkg-config --cflags --libs jack` */ #include <stdio.h> #include <unistd.h> // for sleep() #include <jack/jack.h> /* These variables will hold created jack ports. We use globals so process() function can access them. If you want to avoid using globals, make use of user defined argument of process(). See jack_set_process_callback() how to pass data to process() function. */ jack_port_t *in; jack_port_t *out; /* This function will be run in separate thread by jack to process audio samples. Samples are fed in chunks eg. 1024 samples per call. nframes param tells how many samples are to be processed. It processes samples in real time, so it shall not make any prints, file reads etc. */ int process(jack_nframes_t nframes, void *arg) { float *input_buffer = (float*)jack_port_get_buffer(in, nframes); float *output_buffer = (float*)jack_port_get_buffer(out, nframes); // copy samples from input to output port, raising amplitude two times for(int i=0; i<nframes; ++i) { output_buffer[i] = input_buffer[i] * 2.0; } return 0; } int main(void) { // connect to jack server jack_client_t *client = jack_client_open("jack-dev-2", JackNoStartServer, NULL); if(client == NULL) { fprintf(stderr, "Connecting to jack failed\n"); return 1; } // create input port in = jack_port_register(client, // client for which to create port "in", // name of a port JACK_DEFAULT_AUDIO_TYPE, // port type telling what kind of data it handles JackPortIsInput, // port flags, most important is telling if this port is input or output 0); // not used, parameter ignored for builtin type ports if(in == NULL) { fprintf(stderr, "Creating input port failed\n"); jack_client_close(client); return 1; } // create output port out = jack_port_register(client, "out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if(out == NULL) { fprintf(stderr, "Creating output port failed\n"); // ports are unregistered automatically on client close, so there is no need to unregister in port jack_client_close(client); return 1; } // register process function jack_set_process_callback(client, process, NULL); // start processing audio jack_activate(client); // wait indefinitely /* the only way to stop this sleep is to kill program, so there is no need to worry about disconnecting from jack */ sleep(-1); return 0; }
To process audio in jack you have to create ports throu which samples are delivered to you and received from you. In this client we only create 1 input port and 1 output port. If you wish to eg. play stereo sound you shall create two output ports. Remember that port names within single program must have unique names. If they have not, jack will refuse to create port. You can name your stereo ports eg. left and right or out_1 and out_2 as many programs do.
Actual processing of audio data is done in function registered by jack_set_process_callback()
.
If you wish to pass any arguments to processing function, you can use third parameter of
jack_set_process_callback()
. Here we ignore it.
Processing callback function (here named just process()
) is called by jack in separate thread
every time there is job to be done. To get samples from ports it must have access to
ports created in main()
. Here we make it possible by using global variables.
Process fuction only recevies number of samples to process (nframes
parameter).
To get actual samples we need to get buffers associated with ports by using jack_port_get_buffer()
.
Returned buffer is just and array of nframes
samples. Every sample in jack is a 32bit floating
point number in range -1.0 to 1.0.
Buffers shall be acquired every time process is called and shall not be cached as jack documentation notifies
-
jack_port_get_buffer()
docs.
Actual processing of audio data is started after calling jack_activate()
. From this point
jack keeps pushing data into input port and receives data from output port.
Remember that process()
function shall operate in real time. So any potencially time-costly
operations shall not be executed in this function (print, files, networking etc.).
If your function takes too long to execute jack may decide to kick your processing function out of
processing chain. Some statistics about execution time can be acquired by looking at
jack_cpu_load
.
Jack clients (njconnect, qjackctl) usually also show this value somewhere.
In this third program you will:
You will learn how to:
Program source:
/* Generate pure tone on output. Compile with: cc -o jack-dev-3 jack-dev-3.c `pkg-config --cflags --libs jack` -lm Notice additional flag -lm for math library */ #include <stdio.h> #include <unistd.h> // for sleep() #include <math.h> #include <jack/jack.h> jack_port_t *out; float sample_rate; float sample_number = 0; const float sine_frequency = 256; // Hz int process(jack_nframes_t nframes, void *arg) { float *output_buffer = (float*)jack_port_get_buffer(out, nframes); for(int i=0; i<nframes; ++i) { float time = sample_number / sample_rate; output_buffer[i] = sinf(2.0*M_PI * sine_frequency * time); ++sample_number; } /* 32bit float has only 24 bits to store digits of number (significand), which will overflow in less than 6 minutes when using 48kHz sample rate to track sample number (2^24/48000/60). To prevent this make sample_number cyclic, as sine(2pi*f*t) function does not care if time = 2.3 or 0.3. */ if(sample_number > sample_rate) { sample_number -= sample_rate; } return 0; } int main(void) { jack_client_t *client = jack_client_open("jack-dev-3", JackNoStartServer, NULL); if(client == NULL) { fprintf(stderr, "Connecting to jack failed\n"); return 1; } // get sample rate for use in process() sample_rate = (float) jack_get_sample_rate(client); out = jack_port_register(client, "out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if(out == NULL) { fprintf(stderr, "Creating output port failed\n"); jack_client_close(client); return 1; } jack_set_process_callback(client, process, NULL); jack_activate(client); sleep(-1); return 0; }
The easiest way to track time in processing function is to keep counting samples. Every sample lasts for 1/sample_rate seconds (for sample_rate=48000Hz this gives 0.000021s). When counting samples you have to keep in mind that counter may overflow. So either make counter big enough for the time in which program will run or make counter wrap values after some specified number of samples. In this program we use second approach.
One more notice - you may be tempted to use system time services, but remember that you are not processing one sample at a time but eg. 1024. So system's current time and time of a given sample do not have much in common.
To generate pure tone we need to generate sine wave at a given frequency. This is just as easy as plugging in sin(2pi*f*t) into processing function and writing its values to output port.
To use sinf()
we need to use math libary but it is not linked by default during compilation.
We have to add it manually by using -lm in compilation command.
Article written 04.12.2016
Update 05.01.2017: Some distrubutions require to install additional developer package to get header files.