Organn

A simple drawbar organ in Rust

Organn

A Rust application that roughly models a drawbar organ.

Largely an experiment in audio coding, multithreading and FFI.

Why?

Language features looked interesting for audio

Concurrency is becoming more important

An organ seemed a good size for a spare time project

What is Rust?

Rust is a systems programming langauge from Mozilla, aimed at:

  • Memory safety
  • Concurrency
  • Performance

Features

  • Ownership and move semantics everywhere
  • Lifetime restrictions enforced by compiler
  • Traits, like interfaces but used by generics
  • Non-gc resource management
  • Generally feels a bit higher level than C family


use std::string::String;

fn main() {
    for num_bottles in (1u32..100).rev() {
        println!("{}", bottles_line(num_bottles, true));
        println!("{}", bottles_line(num_bottles, false));
        println!("Take one down, pass it around...");
        println!("{}", bottles_line(num_bottles - 1, true));
        println!("-----------------------------------");
    }
}

fn bottles_line(num_bottles: u32, on_the_wall: bool) -> String {
    let tail = match on_the_wall {
        true => "of beer on the wall!\n",
        false => "of beer\n"
    };

    match num_bottles {
        0 => format!("No bottles {}", tail),
        1 => format!("One bottle {}", tail),
        n => format!("{} bottles {}", n, tail)
    }
}
						

Source, Playground

Rust Audio Ecosystem

Basic facilities are there

Audio IO libraries: CoreAudio, PortAudio, OpenAL etc

Utilities: pitch_calc, panning etc

Midi on most platforms

Notable omission

No CoreMidi library yet

Had to write my own FFI wrapper

Drawbar organ

A number of sine-ish oscillators per note (usually 9)

Drawbars control the mix of different harmonics

Notes are on or off, no envelope, no velocity

Image by Wikipedia user D135-1r43

Structure

Sound generation is split into voice threads

Each thread runs a number of voices

Per-thread mixing of voices

Final mixing of audio from all threads in audio output thread

Midi input callback forwards midi to voices

Voice Structure

9 Oscillators

Mixer

Amp/envelope

Gritty details

Audio buffers

Connections

Audio objects

Audio buffers

Just short arrays


pub const BUFFER_SIZE: usize = 16;
pub type AudioBuffer = [f32; BUFFER_SIZE];
pub const BLANK_BUFFER: AudioBuffer = [0.0; BUFFER_SIZE];
						

Connections

Traits for output and input ends


pub trait Output {
    /// Write audio into the buffer
    fn supply_audio(&self, buffer: AudioBuffer);
}

pub trait Input {
    /// Get audio out of the buffer
    fn get_audio(&self) -> AudioBuffer;
}
						

Connections

2 implementations:

  • Unthreaded - faster, can only be used within a thread
  • Threaded - can move audio between threads

Both are thin wrappers around standard libray objects.

Audio objects

Individual audio processing stages are objects.

Use generics to accept different connection types


pub struct Mixer<T, U> where T: Input, U: Output {
    levels: Vec<f32>,
    inputs: Vec<T>,
    output: U
}
						

Audio objects

Each object has a run method for performing processing


pub fn run(&mut self) {
    let mut samples: AudioBuffer = BLANK_BUFFER;

    for (input, level) in self.inputs.iter().zip(self.levels.iter()) {
        let in_samples = input.get_audio();
        for (sample, in_sample) in samples.iter_mut().zip(in_samples.iter()) {
            *sample += *in_sample * level;
        }
    }

    self.output.supply_audio(samples);
}
						

Oscillator

Contains a phase iterator object which produces phase values for each sample

32 bit counter incremented (with wrapping) every sample by an amount based on the frequency

Has selectable scaling of output values

Oscillator

Uses a phase iterator scaled to 2π

Applies sine funtion to it

Mixer

Contains a vector of inputs and a vector of level settings

Multiplies each input by its level setting and adds them up

Amp/Env

Simple short attack-release envelope for avoiding pops and clicks

Has a ramp time in samples and a current position that is updated every sample during attack and release

Input signal is multiplied by position/ramp time

Audio processing chain

Built by joining objects with connections

Code corresponds to structure diagram


for _ in (0..9) {
	let (output, input) = unthreaded_connection::new();
	let osc = Oscillator::new(sample_rate, output);

	oscillators.push(osc);
	osc_connections.push(input);
}

let num_oscs = osc_connections.len();
let (mix_output, env_input) = unthreaded_connection::new();
let mut mixer = Mixer::new(osc_connections, vec![0.0; num_oscs], mix_output);

// mixer setup omitted

let env = Env::new(env_input, voice_output, 20, sample_rate);
						

Audio processing is done by calling the run method on each object in the signal chain.


for osc in self.oscillators.iter_mut() {
	osc.run();
}
self.mixer.run();
self.env.run();
						

This allows for processing to be moved to a different thread if desired

Results

It works!

32 voices on 4 threads

Threading adds latency (16 samples)

Next steps

Add chorus or rotary speaker simultation

Switch from midi to an internal message format and add a midi mapper

Links

Code - https://github.com/monsieursquirrel/organn

Rust language - https://www.rust-lang.org/

Article on oscillators - http://0au.de/2015/07/numerically-controlled-oscillators/

Thank You