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.
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 ...
}
...
}
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:
access the buffers directly by their index number, via methods JJackAudioEvent.getInput(index)
and JJackAudioEvent.getOutput(index)
.
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();
float
:-1.0f
to +1.0f
.
int
and float
values when passing audio data from one client
to another.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++) {
...
}
}
}
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)
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:libjjack.so
, which
implements the native interface methods of JJackSystem
.
(To make sure the Java runtime environment can find the native library
file, set the Java system property java.library.path
. See
JJack manual, Environment,
Library Path.)jack_connect
or qjackctl
. (Note that the JJack system can also
autoconnect itself to physical ports on startup. See JJack manual, JJack System
Properties)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.)
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
CLIENTSThe JJack API proposes three event-driven mechanisms to interconnect
audio processing clients with each other:
chained
-event-source of a de.gulden.framework.jjack.
JJackMonitor
or de.gulden.framework.jjack.
JJackClient
(method setChained(..)
).monitor
-event-source (method addMonitor(..)
).de.gulden.framework.jjack.JJackProcessListener
interface. This last option is usually used for internal control or
debugging purposes, not for building audio processor clients.(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.
JJack clients fit well into the JavaBeans concept for two main
reasons:
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.
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.