In this article I’ll present some bits of the work we’ve been doing daniel309 (author of jVSTwRapper) and I, around the topic: is it possible to use Ruby to make it easier to prototype VST plugins?
It looks like yes – here’s the most simple example:
class RubyGain < OpazPlug
plugin "RubyGain", "Opaz", "LoGeek"
can_do "1in1out", "plugAsChannelInsert", "plugAsSend"
unique_id "RGaN"
param :gain, "Gain", 1.0, "dB"
def process(inputs, outputs, sampleFrames)
inBuffer, outBuffer = inputs[0], outputs[0]
for i in (0..sampleFrames-1)
outBuffer[i] = inBuffer[i] * gain
end
end
endWe’ve been working iteratively using Git, and a few things came out:
- a DSL that allows to use declarative syntax for parameters handling (which usually involves a lot of boilerplate code)
- the possibility to mix and match Java (for performance) and JRuby (for conciseness and declarativeness) as needed
- a set of rake tasks to make it easier to bundle the plugins for the 3 platforms (Windows, Mac OS X, Linux)
- some simple but working, portable plugins
- other promising things like UI DSL, IRB console for live code hacking
- a lot of fun :)
I’ll keep how things work internally for a later post (a lot of glueing), here I’ll present the basics and examples of plugins, but now I’m totally sold on the fact that JRuby is a great way to leverage the existing JVM infrastructure.
The overall performance is very slow compared to the Java or C++ equivalent, but it provides a very agile way to try out ideas before optimizing and picking either Java or C++ once the idea is mostly there.
Some definitions to get started
- VST – one of the most common norms to create audio plugins, supported by hosts like Ableton Live, Reaper and many more
- VST SDK – a C++ SDK provided by Steinberg to develop VST plugins
- jVSTwRapper – a Java VST framework, sitting on top of the VST SDK and allowing to create plugins with Java in a portable fashion
- Opaz-PlugDK – a JRuby DSL + set of rake tasks, sitting on top of jVSTwRapper, allowing to create portable VST plugins with JRuby (and Java when better performance is needed)
How to use the simple gain
If you git clone the opaz-plugdk repo, you’ll see that all plugins are under a plugins folder.
The following task (you’ll need javac in the path) will build the 3 versions of the plugins:
$ rake package plugin=RubyGain
[snip]
$ ls plugins/RubyGain/build/
linux osx winThese three folders contain a RubyGain.vst folder ready to be added to the VST folder configured in your host.
Improving the performance: HybridGain
Our rake tasks automatically compile the .java files when rake package is called. Here’s a notably faster version of the previous gain – it mixes JRuby code (for the declaration and glue code) with Java (for CPU intensive code).
HybridGain.rb
include_class Java::HybridGainTools
class HybridGain < OpazPlug
plugin "HybridGain", "Opaz", "LoGeek"
can_do "1in1out", "plugAsChannelInsert", "plugAsSend"
unique_id 9876549
param :gain, "Gain", 1.0
def process(inputs, outputs, sampleFrames)
HybridGainTools.applyGain(inputs[0], outputs[0], sampleFrames, gain)
end
endHybridGain.java
// factor out computation-intensive stuff to java
public class HybridGainTools {
public static void applyGain(
float[] input, float[] output,
int sampleFrames, float gain) {
for (int i=0; i < sampleFrames; i++) {
output[i] = gain * input[i];
}
}
}Thanks to JRuby, it’s quite easy to delegate the CPU intensive stuff to Java – we can use the java classes straight from the ruby code.
A more useful example: HybridFilta
The hybrid filter is a LP/HP cut-off resonance filter (quite used in “filtered house” for instance). Although longer, the code is still an order of magnitude shorter than a full Java or full C++ equivalent plugin.
HybridFilta.rb
include_class Java::FilterTools
class HybridFilta < OpazPlug
plugin "HybridFilta", "Opaz", "LoGeek"
can_do "1in1out", "plugAsChannelInsert", "plugAsSend"
unique_id "hflt"
param :cut_off, "Cut Off", 1.0
param :resonance, "Resonance", 0.1
param :mode, "Mode (LP or HP)", 0
def filter
@filter ||= FilterTools.new
end
def use_low_pass?
mode < 0.5
end
def process(inputs, outputs, sampleFrames)
filter.recomputeParameters(cut_off, resonance, use_low_pass?, sample_rate)
filter.apply(inputs, outputs, sampleFrames)
end
endHybridFilta.java
public class FilterTools {
float f,r,c,a1,a2,a3,b1,b2;
public void recomputeParameters(
float cutoff, float resonance,
boolean lowPassMode, float sampleRate) {
// r = rez amount, from sqrt(2) to ~0.1
r = (1-resonance) * 10f;
if (r<0.1f) r=0.1f;
f = cutoff * sampleRate/4;
if (f<40f) f=40f;
if (lowPassMode) {
c = (float)(1.0 / Math.tan(
Math.PI * f / sampleRate));
a1 = 1.0f / (1.0f + r*c + c*c);
a2 = 2*a1;
a3 = a1;
b1 = 2.0f * (1.0f - c*c) * a1;
b2 = (1.0f - r*c + c*c) * a1;
} else { // hipass mode
c = (float)Math.tan(
Math.PI * f / sampleRate);
a1 = 1.0f / (1.0f + r*c + c*c);
a2 = -2*a1;
a3 = a1;
b1 = 2.0f * (c*c - 1.0f) * a1;
b2 = (1.0f - r*c + c*c) * a1;
}
}
// history: input n-y, output n-y
float ih1_1=0, ih1_2=0, oh1_1=0, oh1_2=0;
float computeFilter(
float inp0, float inp1, float inp2,
float outp1, float outp2) {
return a1*inp0 + a2*inp1 + a3*inp2
- b1*outp1 - b2*outp2;
}
public void apply(float[][] inputs,
float[][] outputs, int sampleFrames) {
float[] input0 = inputs[0];
float[] output0 = outputs[0];
output0[0] = computeFilter(
input0[0], ih1_1, ih1_2, oh1_1, oh1_2);
output0[1] = computeFilter(
input0[1], input0[0], ih1_1,
output0[0], oh1_1);
for (int s=2; s<sampleFrames; s++)
output0[s] = a1*input0[s]
+ a2*input0[s-1] + a3*input0[s-2]
- b1*output0[s-1] - b2*output0[s-2];
// save history
ih1_1 = input0[sampleFrames-1];
ih1_2 = input0[sampleFrames-2];
oh1_1 = output0[sampleFrames-1];
oh1_2 = output0[sampleFrames-2];
}
}Going further: implementing VST instruments
Instruments are plugins that take midi in and generate audio (short version). We implemented a simple synth here – overall I find that Ruby is nice to handle the midi events and logic.
Where do we go from here?
It’s been an interesting road. Getting access to the libraries provided by both Java and Ruby, coding in JRuby as long as it’s beneficial and optimizing when needed is definitely interesting. And I wouldn’t have achieved anything if I hadn’t reused the existing infrastructure (jVSTwRapper makes all the heavy lifting here).