I built this to find out how far I could push the JVM for real-time audio, and to learn digital signal processing properly by writing it myself.
What I built
June’s Logue is a polyphonic synthesiser in Java with a JavaFX interface. I wrote the engine by hand, with no third-party DSP libraries. Each voice has an oscillator (sine, saw, triangle or square), a resonant low-pass filter, and two ADSR envelopes, one for volume and one for the filter cutoff. A global LFO handles stereo panning, and voice stealing makes sure a new note always gets a voice. It reads MIDI note, velocity and control change messages, so I played it from Logic Pro.
Keeping the audio loop clean
A synthesiser has to compute sound faster than you can listen to it. At 44,100 samples a second, in blocks of 256 samples, the engine has about 5.8 milliseconds to compute each block before the sound card runs dry and you hear a click. That is a real engineering constraint, and it breeds creativity in algorithm and system design: the kind of constraint I love working within. In Java, a garbage collection pause at the wrong moment is exactly that click.
The engine never creates objects while it plays. Every object is allocated once, at start-up, and reused throughout the life cycle of the program, the way an object pool works, so the garbage collector has nothing to clean up.
Expensive calculations are performed during start-up rather than at runtime. The waveforms and the filter coefficients are worked out once and stored in lookup tables, so the loop does lookups instead of calling Math.sin or Math.tan. Smaller things add up. The oscillator’s phase wraps with a bitwise AND instead of a modulo, and values like one over the sample rate are calculated once, so the loop multiplies where it would have divided.
What it showed me
This is the project that taught me how much a language’s memory model decides its performance: memory managed by hand in C++, garbage collected in Java, or tracked through lifetimes in Rust. Even with all that care, the JVM still underperforms a language with leaner memory management: under identical test bench conditions (8 voices, 2,000 audio blocks), the Rust port did the same work in about 45% less time. Careful code lowers the risk of a collector pause and doesn’t remove it, which is why C++ or Rust is the usual choice for something like this. As a way to learn what real-time code costs, it was worth it.
The version you’re playing
The demo above runs a Rust port of the engine, compiled to WebAssembly so it works in a browser. I made the port with agents rather than by hand (thanks, Claude!). It has the same structure as the Java version, and Rust has no garbage collector, so the problem I worked around in Java doesn’t arise. The interface is a copy of the Java one, so what you see is the original design and user experience.