March 25, 2017 · Rust Music

Writing an Audio Plugin in Rust

The code from this tutorial is available on GitHub.

Along the long and slightly convoluted path I took to finding my current career as a software developer, I ended up spending a year at college studying music. It didn't really amount to much in the way of gainful employment, but it's still something I'm really passionate about, and I'd really like to find more ways of combining it with my love of programming.

So when I stumbled across the vst2 Rust crate (created by the delightfully named 'overdrivenpotato'), I couldn't resist taking some time out of my Saturday to check it out.

There's always a relevant XKCD.

I'm no audio programming wizard, so I'm going to start really simple by throwing together a bare-bones digital distortion effect, based on the algorithm from this tutorial by Martin Finke.

Prerequisites

To follow along with this tutorial, you'll need:

It's worth noting that I'm running the 64-bit version of Ableton on Windows - if you're using a 32-bit plugin host, you'll need to compile your plugin with the 32-bit Rust toolchain. If you're on Mac, you can use zywicki's handy shell script to package the library in a format that will work for you.

The Bare Minimum

Let's start as simple as possible, by getting a completely empty VST plugin loading into our host. All it'll do is pass through audio unaltered - not very exciting, but it'll prove our code works.

First, create a new Rust library using Cargo:

cargo new digidist  
cd digidist  

In our cargo.toml, let's add a dependency on vst2. While we're at it, we'll also set the type of our crate to dylib, so that the compiler will output a dynamic library.

Cargo.toml
[package]
name = "digidist"  
version = "0.1.0"  
authors = ["Joe Clay <27cupsofcoffee@gmail.com>"]

[dependencies]
vst2 = "0.0.1"

[lib]
crate-type = ["dylib"]  

Now we can actually start laying the groundwork for our plugin. In order for the vst2 crate to work its magic, we need to do three things:

With all that in place, our code should look something like this:

lib.rs
#[macro_use] extern crate vst2;

use vst2::buffer::AudioBuffer;  
use vst2::plugin::{Plugin, Info};

#[derive(Default)]
struct DigiDist;

impl Plugin for DigiDist {  
    fn get_info(&self) -> Info {
        Info {
            name: "DigiDist".to_string(),
            vendor: "17cupsofcoffee".to_string(),
            unique_id: 25032017,

            // fill in the rest with the default values
            ..Info::default()
        }
    }
}

plugin_main!(DigiDist);  

Believe it or not, that's all that's required to create our bare minimum VST! Run cargo build, and then copy target/debug/digidist.dll into your host's plugins folder. All being well, you should be able to load it onto a track:

Our empty plugin running in Ableton

Audio Mangling

Before we start writing our actual audio processing code, a quick warning - make sure that you turn down your volume, and place a limiter on your host's audio output when developing VSTs! Your ears will thank you the first time you run into a bug.

Our digital distortion plugin is going to have:

We can add this information to our plugin with very little effort:

lib.rs
impl Plugin for DigiDist {  
    fn get_info(&self) -> Info {
        Info {
            name: "DigiDist".to_string(),
            vendor: "17cupsofcoffee".to_string(),
            unique_id: 25032017,

            inputs: 2,
            outputs: 2,
            parameters: 1,

            ..Info::default()
        }
    }
}

We're going to have to store that parameter somewhere, so let's add a field to our struct. While we're at it, we'll also manually implement Default, so that our threshold slider will start at the maximum value (i.e. no distortion) rather than at zero.

lib.rs
struct DigiDist {  
    threshold: f32
}

impl Default for DigiDist {  
    fn default() -> DigiDist {
        DigiDist {
            threshold: 1.0
        }
    }
}

In order to link this field up to our VST's interface in the host application, we need to implement five methods from the Plugin trait, which all take an integer index:

For our plugin, the implementations will look like this:

lib.rs, inside 'impl Plugin for DigiDist'
fn get_parameter(&self, index: i32) -> f32 {  
    match index {
        0 => self.threshold,
        _ => 0.0,
    }
}

fn set_parameter(&mut self, index: i32, value: f32) {  
    match index {
        // We don't want to divide by zero, so we'll clamp the value
        0 => self.threshold = value.max(0.01),
        _ => (),
    }
}

fn get_parameter_name(&self, index: i32) -> String {  
    match index {
        0 => "Threshold".to_string(),
        _ => "".to_string(),
    }
}

fn get_parameter_text(&self, index: i32) -> String {  
    match index {
        // Convert to a percentage
        0 => format!("{}", self.threshold * 100.0),
        _ => "".to_string(),
    }
}

fn get_parameter_label(&self, index: i32) -> String {  
    match index {
        0 => "%".to_string(),
        _ => "".to_string(),
    }
}

Now for the exciting part - let's mangle some audio! The algorithm for this distortion effect is going to be very, very simple:

pseudo-code
if input >= 0  
  output = min(input, threshold) / threshold
else  
  output = max(input, -threshold) / threshold

Thanks to the nicely designed buffer API provided by the vst2 crate, our actual processing code looks pretty much the same!

lib.rs, inside 'impl Plugin for DigiDist'
fn process(&mut self, buffer: AudioBuffer<f32>) {  
    // Split out the input and output buffers into two vectors
    let (inputs, outputs) = buffer.split();

    // For each buffer, transform the samples
    for (input_buffer, output_buffer) in inputs.iter().zip(outputs) {
        for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {

            if *input_sample >= 0.0 {
                *output_sample = input_sample.min(self.threshold) / self.threshold;
            }
            else {
                *output_sample = input_sample.max(-self.threshold) / self.threshold;
            }

        }
    }
}

That's it, we're done! Compile your code, install the DLL into your host, and let's see how it looks and sounds. Turn down your volume - Soundcloud's embeds are always way too loud!

Look, parameters!

Hm. I'm not exactly Aphex Twin, am I? But it's a start!

Wrapping Up

Hopefully you found this quick tour of the vst2 crate as fun as I did - I'm floored by how quickly I was able to go from "hey, this sounds like it'd be a cool idea" to having a working plugin!

Honestly, that's my experience of Rust in a nutshell. This language makes so many of the things that used to only be the realm of experienced C++ programmers accessible to the rest of us - I find that really exciting!

The code from this tutorial is available on GitHub.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus