Skip to content

Configuring for two way JNI communication

Igor Zinken edited this page Apr 29, 2020 · 12 revisions

The application build files have been written to be compiled using SWIG, allowing the C++ code to be wrapped for use in Java, using JNI (Java Native Interface).

Using JNI provides the benefit that the audio engine modules can be constructed and manipulated within a Java application, while still being executed in a native layer outside of the Java VM for optimum performance. Using SWIG allows the source to be written in pure C++ in accordance with common guidelines, without having to write code explicitly for use with Java, as it is up to SWIG to provide the wrappers. This provides an additional benefit that MWEngine can be used solely within the context of a C(++) application without any additional bloat.

Two-way communication

You can write a perfectly fine audio application without having to worry about receiving information from the native layer. For instance when you merely want to instantiate existing modules and adjust their properties. Through SWIG all modules are available as either static methods or classes. For instance you could start the engine like so:

MWEngine engine = new MWEngine( getApplicationContext(), IObserverImplementation );
engine.createOutput( 44100, 512, 2, Drivers.types.OPENSL );
engine.getSequencerController().updateMeasures( 1, 16 ); // we'll loop just a single measure
engine.start();

And quickly create an synthesis environment like so:

SynthInstrument synthesizer = new SynthInstrument();
synthesizer.setWaveform( 2 );
Delay delay = new Delay( 250f, 2000f, .35f, .5f, 1 );
synthesizer.getProcessingChain().addProcessor( delay );

...continue by adding AudioEvents, starting the sequencer, etc. etc.

It can however be desirable to also receive messages from the engine (for instance: to follow changes in the sequencers playback position so you can update the state of a progress bar / create custom notifications, etc.). MWEngine can do this task through JNI using the JavaBridge (javabridge.h). The engine by default contains some messages that are broadcast on step position update, change in recording state, on initialization failure, etc. to a Observer-class (observer.h) instance.

If you choose to use JNI in your build, there are already some default methods available to catch messages through JNI onto the nl.igorski.mwengine.MWEngine-class' IObserver-instance which acts as the mediator between the Java layer and the native layer.

So onto:

Configuring the build for different scenarios

Scenario 1. "So I can run high performance native code with Java ? Yes please!"

Excellent. To ensure the JavaBridge is available, firstly make sure the following compiler directive is present in global.h :

#define USE_JNI

...to add some JNI specific operations to the AudioEngine and the Observer so they automatically broadcast the default messages (to the MWEngine Java class - which is a standard part of the MWEngine library and by default defined in javabridge.h as the mediating class -). Note, this is ON by default.

Secondly, you must make sure that mwengine.i (the descriptor file that tells SWIG which native classes to make available to JNI) contains the following includes:

#include "javabridge_api.h"
%include "javabridge_api.h"

which is a header file that contains methods to manipulate the AudioEngine's thread via proxy (the reason for this is that the proxied method allows us to grab a reference to the Java VM (and the calling Java object, which by default is the mediating class MWEngine) prior to starting the method of the same name in the AudioEngine. This allows us to update the reference to the Java environment to overcome concurrency errors when the Java environment is threading. Once more, this is ON by default.

And finally ensure that the following instruction is uncommented in CMakeLists.txt :

set(JNI_SOURCES ${CPP_SRC}/jni/javabridge.cpp
                ${CPP_SRC}/jni/javautilities.cpp)

to ensure that the JavaBridge is part of the compiled library. Note that by not definining the javabridge.h file in the mwengine.i-file the JavaBridge is not accessible in Java, and it shouldn't be! The Java side merely needs to ensure that it provides public static methods for the native layer to call (and these are mapped in javabridge.h).

A note on garbage collection

Whenever you construct a native object in Java, keep in mind that the garbage collector is keeping an eye on the reference! If the reference to an object in Java is broken, it is eligible for garbage collection. Prior to disposal of the object, the garbage collector will finalize the object and then clear memory. In SWIG this finalization process will also invoke the C++ destructor. As such, be aware that the object that is about to be finalized isn't in actual use by the native layer anymore!

Consider this example:

SynthInstrument _synthesizer;

public void Foo()
{
    _synthesizer = new SynthInstrument();
    _synthesizer.getProcessingChain().addProcessor( new Delay( 250f, 2000f, .35f, .5f, 1 ));
}

Here we have a reference to a member variable synthesizer which inside function "Foo" we construct as a new instance of SynthInstrument. We then create a new Delay-module inline and add it to the synthesizers processing chain.

The Delay now exists within the native layer and as it is part of a processing chain, it is in use by the engine to render the output of the AudioChannel the SynthInstrument belongs to. However: if the body of the Java function "Foo" exits, the reference to Delay is lost and it will be picked up by the garbage collector whenever its next cycle is scheduled. When it is finalized by the garbage collector, the audio engine is unaware of this and it still has a pointer to a BaseProcessor (the Delay) inside the AudioChannels ProcessingChain. When it tries to apply the processor to the channel it will cause a crash as the ProcessingChain now points to deleted memory. Whoops. :(

As such, hold a strong reference to these modules that are constructed on the Java side, and when disposing of them, also take the necessary steps to dispose of them in the audio engine. For the above example, simply create a member variable for the Delay and once you decide it is of no use anymore, invoke:

_synthesizer.getProcessingChain().removeProcessor( _referenceToDelay );

prior to nullifying the actual referenceToDelay-Object.

ProGuard

When obfuscating and minimizing the Java code for a production build, keep in mind that the methods callable from the native layer (defined in MWEngine.java) should NOT be removed NOR renamed. The javabridge.h requires a specific signature for the Java side of things, otherwise communication will fail!

Scenario 2. "I will be using the MWEngine solely from C++"

And why not? In global.h simply make sure USE_JNI isn't defined and update the makefile to omit using SWIG to compile a purely native library.

Clone this wiki locally