Developing JJack Clients

This document roughly describes how to develop JACK clients in the Java programming language using the JJack API.

Copyright © Jens Gulden.
Licensed under the GNU Lesser General Public License (LGPL).

This software comes with NO WARRANTY. See file LICENSE for details.


CONTENTS

SINGLE CLIENTS

INTERCONNECTABLE CLIENTS

USING JJACK CLIENTS AS JAVABEANS



SINGLE CLIENTS

Process method

The most simple kind of JJack client is any Java class that implements interface de.gulden.framework.jjack.JJackAudioProcessor. There is only a single method that needs to be implemented:

import de.gulden.framework.jjack.*;

public class MyJJackClient extends ... implements JJackAudioProcessor {

    ...

    /**
     * Process multiple samples from input buffer to output buffer.
     * @param e event object with references to input buffer and output buffer.
     */

    public void process(JJackAudioEvent e) {

        ... code to create, manipulate or analyse audio waveform ...
    }
    ...
}

Accessing audio data

Access to audio data is handled through buffers of type java.nio.FloatBuffer. There is one FloatBuffer for each input channel, and one for each output channel.

Each time the process()-method is called, a number of audio samples is passed via the input and output buffers of the JJackAudioEvent. The number of samples may differ each call. The output buffer is expected to be filled with exactly the same number of samples as passed in via the input buffer.

It is up to the client whether it uses values of the input data to generate an output waveform, or to completely ignore outputs (as clients would do that only monitor incoming data) or inputs (as clients that create audio data would do).

There are two options for accessing the FloatBuffers of input and output ports:

  1. access the buffers directly by their index number, via methods JJackAudioEvent.getInput(index) and JJackAudioEvent.getOutput(index).

  2. access the buffers indirectly through the channel model that comes with JJack:
    JJackAudioChannel ch = audioEvent.getChannel( 0 );
    JJackAudioPort port = ch.getPort( JJackConstants.INPUT );
    FloatBuffer buf = port.getBuffer();

Audio data format

Note that in the JACK world audio waveform data is represented as floating-point values of type float:
This makes it possible for JACK and its clients to handle data independently from the bit-reslution of the underlying audio hardware. It also prevents several steps of conversion between int and float values when passing audio data from one client to another.

Examples

A typical process()-method loops over all channels available, and performs its operation by subsequently reading data from the input buffers and writing data to the output buffers.

The following example is taken from the Gain-client (class de.gulden.application.jjack.clients.Gain), included in JJack's distribution archive:

    public void process(JJackAudioEvent e) {
        float v = getVolume();
        for (int i=0; i<e.countChannels(); i++) {
            FloatBuffer in = e.getInput(i);
            FloatBuffer out = e.getOutput(i);
            int cap = in.capacity();
            for (int j=0; j<cap; j++) {
                float a = in.get(j);
                a *= v;
                if (a>1.0) {
                    a = 1.0f;
                } else if (a<-1.0) {
                    a = -1.0f;
                }
                out.put(j, a);
            }
        }
    }   

The second example originates from the Delay-client (class de.gulden.application.jjack.clients.Delay), also included in the distribution archive:

    public void process(JJackAudioEvent e) {
        int delaytime = getTime();
        float mixSignal = (float)getMixSignal() / 100;
        float mixFx = (float)getMixFx() / 100;
        float outSignal = (float)getOutSignal() / 100;
        float outFx = (float)getOutFx() / 100;
        int sampleRate = getSampleRate();
        int diff = delaytime * sampleRate / 1000 ;
        int channels = e.countChannels(); // number of channels (assumes same number of input and output ports)
        if (ring == null) { // first call, init ringbuffers for each channel
              ring = new RingFloat[channels];
              for (int i = 0; i < channels; i++) {
                  ring[i] = new RingFloat(diff);
              }
        }
        for (int i=0; i < channels; i++) {
            RingFloat r = ring[i];
            r.ensureCapacity(diff);
            FloatBuffer in = e.getInput(i);      // input buffer
            FloatBuffer out = e.getOutput(i); // output buffer
            int cap = in.capacity(); // number of samples available
            for (int j=0; j<cap; j++) {
                float signal = in.get(j); // read input signal
                float fx = r.get(diff);
                float mix = signal * mixSignal + fx * mixFx;
                float ou = signal * outSignal + fx * outFx;
                r.put(mix); // remember for delay
                out.put(j, ou); // write output signal
            }
        }
    }

This example uses the channels&ports-model of the JJack API (via interfaces de.gulden.framework.jjack.JJackAudioChannel and  de.gulden.framework.jjack.JJackAudioPort) to access the buffers:

    public void process(JJackAudioEvent e) {

        ...


        for (Iterator it = e.getChannels().iterator(); it.hasNext(); ) { // iterate over all channels available

            JJackAudioChannel ch = (JJackAudioChannel)it.next();
            FloatBuffer in = ch.getPortBuffer(INPUT);
            FloatBuffer out = ch.getPortBuffer(OUTPUT);
            for (int i=0; i<in.capacity(); i++) {

                ...

            }

        }
    }

Registering the processor at the underlying JJack system

To link your JJack client into the audio processing chain, an instance of it must be registered at the underlying JJack system. This can e.g. be done inside the main(String[] args)-method of your application:

    MyJJackClient myClient = new MyJJackClient();

    JJackSystem.setProcessor(myClient); // make audio data "flow" through myClient (by subsequent calls to process(..)-method)


Note: Class de.gulden.framework.jjack.JJackSystem implements the underlying JJack system. By the time of static class initilization, that means when the class is accessed for the first time by Java's class loader, the main work of native initialization is performed. The two main steps are:

Note that, depending on the threads-architecture of your system, it might be necessary to let these initialization procedures be performed from the main shell thread (the thread which initially enters the main(String[] args)-method). To force this, perform a dummy-access to JJackSystem from the main-method:

public static void main(String[] args) {
    ...

    Class.forName("de.gulden.framework.jjack.JJackSystem"); // dynamically, class name as String

    or e.g.

    JJackSystem.class.getName(); // statically, dummy call

    ...
}

(This thread-behaviour is also the reason why in some cases, when deploying JJack clients as JavaBeans, it is necessary to start up the BeanBuilder through the JJackSystem wrapper. See Creating JJack Clients with the BeanBuilder.)

Purposes of audio processor clients

There are three basic purposes for which a JJack client can be developed:
  1. Creating audio waveform data. Such a client could e.g. be a sample player, mp3-decoder, synthesizer etc.
  2. Changing audio data, e.g. by amplifying the signal or putting any effect on it.
  3. Analyze audio data, that means, performing some monitoring function on incoming data without generating a signal at all.

When implementing the process()-method, the purpose of a JJack client is reflected only in the way it handles incoming and outgoing data. A client that is creating its own audio data will most likely never call JJackAudioEvent.getInputBuffer(..), while on the other hand a purely monitoring client has no use in calling JJackAudioEvent.getOutputBuffer(..) (or performing the corresponding actions on a JJackAudioChannel object).

Also for a native JACK client, there is no explicit difference between clients that are creating, changing or analyzing audio. Usually, an audio-creating client does not register any input ports, while a monitor client does not need output ports. To achieve the same behaviour for JJack clients, tell the JJack system explicitly how many input and output ports to register. This can either be done by passing a Java runtime property from the command line (e.g. -Djjack.ports.output=0, see JJack manual, JJack System Properties), or by setting these properties programmatically before class de.gulden.framework.jjack.JJackSystem is accessed for the very first time:

public static void main(String[] args) {
    ...
    System.setProperty("jjack.ports.input", "0"); // no inputs, mark this as an audio-creating client only
    ...
    (perform rest of initialization, first access to JJackSystem)
}

INTERCONNECTABLE CLIENTS

The JJack API proposes three event-driven mechanisms to interconnect audio processing clients with each other:

(Note that these mechanisms of interconnection are working inside the Java virtual machine only and have nothing to do with connections between native JACK clients, as configured using tools like jack_connect or qjackctl. To the outside, each Java virtual machine running a JJack system appears as one single native JACK client.)

The event-sources and methods mentioned above are further described in the JJack Javadoc API documentation.

USING JJACK CLIENTS AS JAVABEANS

JJack and the JavaBeans API

JJack clients fit well into the JavaBeans concept for two main reasons:

The BeanBuilder development tool

One JavaBeans-compatible development tool that has been tested with JJack is the BeanBuilder, available for free from Sun Microsystems. See the documentation about Creating JJack clients with the BeanBuilder for a detailed description.

Programming JJack clients as JavaBeans

Any Java class can be treated as a JavaBean, as long as it is serializable and provides a BeanInfo-class. See the JavaBeans API for details.

Every Java class that extends javax.swing.JPanel and implements java.io.Serializable as well as de.gulden.framework.jjack.JJackAudioConsumer|JJackAudioProducer is both a visible JavaBean component and a JJack client.

However, it is useful to derive Bean-compatible JJack clients from either de.gulden.framework.jjack.JJackMonitor or de.gulden.framework.jjack.JJackClient. Using one of these as the superclass reduces work for implementing a new JJack client.See the JJack Javadoc API documentation for details.

Copyright © Jens Gulden and others 2004-2007
Licensed under the LGPL.