## Rust ❤️ Bela – Making Connections

Back from a win­ter hol­i­day break, we re­turn to our se­ries on us­ing Rust with Bela. Last time, we split out our li­brary crate in­to a sep­a­rate pack­age, so we can en­sure the no_std at­tribute is re­spect­ed. Fur­ther­more, we be­gan pars­ing MI­DI events and con­nect­ing them to the vir­tu­al tone wheels. How­ev­er, we did so in the sim­plest pos­si­ble, one-to-one man­ner. Let's fix that.

At this point, we have to look at some ad­di­tion­al de­tails on how tone wheel or­gans work. In the sec­ond part of this se­ries, we had a brief look in­to the in­te­ri­or of such an or­gan and al­ready saw that there are a lot of wires. This time, let's have a look at what the mu­si­cian sees to clar­i­fy some terms.

From top to bot­tom, on­ly look­ing at the main el­e­ments for now, we have:

• The draw­bars, em­u­lat­ing the stops of a pipe or­gan, grouped in two groups of nine, a pair of draw­bars, and two more groups of nine.
• Two key­boards (the up­per and low­er man­u­als) with 12 keys in an in­vert­ed col­or scheme (the pre­set keys) and 61 keys in the usu­al col­or scheme.
• A ped­al­board (ped­al clavier) with 25 ped­als.

Each draw­bar is a switch (not a po­ten­tiome­ter) with nine set­tings num­bered 0 through 8. The ze­ro-set­ting shunts the cor­re­spond­ing sig­nal to ground, while the oth­ers go to the dif­fer­ent taps of a match­ing trans­former with 6, 8, 11, 16, 22, 32, 45, and 64 turns, ap­prox­i­mat­ing a geo­met­ric se­ries with a step of $\sqrt{2}$ . So let's add a lookup ta­ble of gains.

// gains based on
// http://www.dairiki.org/HammondWiki/MatchingTransformer
static DRAWBAR_GAINS: [f32; 9] = [
0.0,
6.0 / 64.0,
8.0 / 64.0,
11.0 / 64.0,
16.0 / 64.0,
22.0 / 64.0,
32.0 / 64.0,
45.0 / 64.0,
1.0,
];


But what about the 9-9-2-9-9 group­ing? Both the up­per man­u­al and the low­er man­u­al have two pairs of nine draw­bars, for two pre­sets cor­re­spond­ing to pre­set keys A♯ and B. The oth­er nine pre­sets (for pre­set keys C♯ through A) are wired on the in­te­ri­or pre­set pan­el and can't be changed while play­ing. The fi­nal pre­set key C is a can­cel key and turns all sig­nals off. The ped­al­board on­ly has two draw­bars and no pre­set ped­als. We'll get to what sig­nals each draw­bar cor­re­sponds to in a mo­ment. For now, we know that we'll need to re­mem­ber the cur­rent draw­bar set­tings and pre­sets, so let's add them as fields to our TonewheelOrgan:

pub struct TonewheelOrgan {
/* ... */
upper_manual_presets: [[u8; 9]; 12],
current_upper_preset: u8,
lower_manual_presets: [[u8; 9]; 12],
current_lower_preset: u8,
pedal_drawbars: [u8; 2],
}

impl TonewheelOrgan {
pub fn new(sample_rate: f32) -> Self {
/* ... */

// presets based on
// - http://www.dairiki.org/HammondWiki/StandardPresets
// - http://www.dairiki.org/HammondWiki/StandardJazzRegistrations
// final two presets are drawbar settings
let upper_manual_presets = [
[0; 9],                      // off / cancel
[0, 0, 5, 3, 2, 0, 0, 0, 0], // Stopped Flute (pp)
[0, 0, 4, 4, 3, 2, 0, 0, 0], // Dulciana (ppp)
[0, 0, 8, 7, 4, 0, 0, 0, 0], // French Horn (mf)
[0, 0, 4, 5, 4, 4, 2, 2, 2], // Salicional (pp)
[0, 0, 5, 4, 0, 3, 0, 0, 0], // Flutes 8' & 4' (p)
[0, 0, 4, 6, 7, 5, 3, 0, 0], // Oboe Horn (mf)
[0, 0, 5, 6, 4, 4, 3, 2, 0], // Swell Diapason (mf)
[0, 0, 6, 8, 7, 6, 5, 4, 0], // Trumpet (f)
[3, 2, 7, 6, 4, 5, 2, 2, 2], // Full Swell (ff)
[8, 8, 8, 8, 8, 8, 8, 8, 8], /* A# preset upper (drawbar
* set 1) */
[8, 8, 8, 0, 0, 0, 0, 0, 4], /* B preset upper (drawbar
* set 2) */
];
let current_upper_preset = 1;
let lower_manual_presets = [
[0; 9],                      // off / cancel
[0, 0, 4, 5, 4, 5, 4, 4, 0], // Cello (mp)
[0, 0, 4, 4, 2, 3, 2, 2, 0], // Flute & String (mp)
[0, 0, 7, 3, 7, 3, 4, 3, 0], // Clarinet (mf)
[0, 0, 4, 5, 4, 4, 2, 2, 0], /* Diapason, Gamba & Flute
* (mf) */
[0, 0, 6, 6, 4, 4, 3, 2, 2], // Great, no reeds (f)
[0, 0, 5, 6, 4, 2, 2, 0, 0], // Open Diapason (f)
[0, 0, 6, 8, 4, 5, 4, 3, 3], // Full Great (ff)
[0, 0, 8, 0, 3, 0, 0, 0, 0], // Tibia Clausa (f)
[4, 2, 7, 8, 6, 6, 2, 4, 4], // Full Great with 16' (fff)
[0, 0, 8, 6, 0, 0, 0, 0, 0], /* A# preset upper (drawbar
* set 1) */
[8, 3, 8, 0, 0, 0, 0, 0, 0], /* B preset upper (drawbar
* set 2) */
];
let current_lower_preset = 1;
let pedal_drawbars = [8, 0];

TonewheelOrgan {
/* ... */
upper_manual_presets,
current_upper_preset,
lower_manual_presets,
current_lower_preset,
pedal_drawbars,
}
}

/* ... */
}


There's a rea­son we saw so many ca­bles on that in­te­ri­or pho­tos. And it gets worse. Run­ning un­der­neath the keys of the man­u­als are nine bus bars each. One for ev­ery draw­bar. Each key has nine pal­la­di­um con­tacts con­nect­ed to nine tone wheels each. Since we on­ly have 91 tone wheels and more than a thou­sand switch­es for the man­u­als alone, not count­ing the ped­al­board, quite a few are reused, some even mul­ti­ple times on a sin­gle keys due to wrap­around. A good over­view on which tone wheels are con­nect­ed to which keys can be found on Jeff Dari­ki's site. These con­nec­tions and the draw­bar la­bels are based on the har­mon­ic se­ries:

Har­mon­icFootage
Sub-Fun­da­men­tal16'
Sub-Third5 ⅓'
Fun­da­men­tal8'
2nd Har­mon­ic4'
3rd Har­mon­ic2 ⅔'
4th Har­mon­ic2'
5th Har­mon­ic1 ⅗'
6th Har­mon­ic1 ⅓'
8th Har­mon­ic1'

The “Sub-Fun­da­men­tal” (an oc­tave be­low the fun­da­men­tal) and “Sub-Third” (a fifth above the fun­da­men­tal) ob­vi­ous­ly aren't ac­tu­al­ly har­mon­ics, but give more flex­i­bil­i­ty in shap­ing the tone. If you're think­ing “this sounds like a prim­i­tive form of ad­di­tive syn­the­sis,” you'd be ex­act­ly right. In any case, the footage la­bels on the draw­bar tabs are based on the footage la­bels on pipe or­gan stops. The fre­quen­cy of a sine is in­verse­ly re­lat­ed to its wave­length, and there­by the length a pipe needs to be to form it.

In any case, we can use the in­for­ma­tion avail­able to store which key needs to be con­nect­ed to which tone wheel, mak­ing sure to ad­just the con­nec­tions to ze­ro-based in­dex­ing and to ac­count for the dum­my tone wheels:

const MANUAL_KEYS: usize = 61;
const PEDAL_KEYS: usize = 25;

// connections based on
// http://www.dairiki.org/hammond/wiring/
// tone wheel numbers (1-91) changed to indices (0-90), same for key
// numbers, final tone wheel indices shifted to account for dummy wheels
static MANUAL_KEY_GENERATORS: [[usize; 9]; MANUAL_KEYS] = [
[12, 19, 12, 24, 31, 36, 40, 43, 48], //  0 C
[13, 20, 13, 25, 32, 37, 41, 44, 49], //  1 C#
[14, 21, 14, 26, 33, 38, 42, 45, 50], //  2 D
[15, 22, 15, 27, 34, 39, 43, 46, 51], //  3 D#
[16, 23, 16, 28, 35, 40, 44, 47, 52], //  4 E
[17, 24, 17, 29, 36, 41, 45, 48, 53], //  5 F
[18, 25, 18, 30, 37, 42, 46, 49, 54], //  6 F#
[19, 26, 19, 31, 38, 43, 47, 50, 55], //  7 G
[20, 27, 20, 32, 39, 44, 48, 51, 56], //  8 G#
[21, 28, 21, 33, 40, 45, 49, 52, 57], //  9 A
[22, 29, 22, 34, 41, 46, 50, 53, 58], // 10 A#
[23, 30, 23, 35, 42, 47, 51, 54, 59], // 11 B
[12, 31, 24, 36, 43, 48, 52, 55, 60], // 12 C
[13, 32, 25, 37, 44, 49, 53, 56, 61], // 13 C#
[14, 33, 26, 38, 45, 50, 54, 57, 62], // 14 D
[15, 34, 27, 39, 46, 51, 55, 58, 63], // 15 D#
[16, 35, 28, 40, 47, 52, 56, 59, 64], // 16 E
[17, 36, 29, 41, 48, 53, 57, 60, 65], // 17 F
[18, 37, 30, 42, 49, 54, 58, 61, 66], // 18 F#
[19, 38, 31, 43, 50, 55, 59, 62, 67], // 19 G
[20, 39, 32, 44, 51, 56, 60, 63, 68], // 20 G#
[21, 40, 33, 45, 52, 57, 61, 64, 69], // 21 A
[22, 41, 34, 46, 53, 58, 62, 65, 70], // 22 A#
[23, 42, 35, 47, 54, 59, 63, 66, 71], // 23 B
[24, 43, 36, 48, 55, 60, 64, 67, 72], // 24 C
[25, 44, 37, 49, 56, 61, 65, 68, 73], // 25 C#
[26, 45, 38, 50, 57, 62, 66, 69, 74], // 26 D
[27, 46, 39, 51, 58, 63, 67, 70, 75], // 27 D#
[28, 47, 40, 52, 59, 64, 68, 71, 76], // 28 E
[29, 48, 41, 53, 60, 65, 69, 72, 77], // 29 F
[30, 49, 42, 54, 61, 66, 70, 73, 78], // 30 F#
[31, 50, 43, 55, 62, 67, 71, 74, 79], // 31 G
[32, 51, 44, 56, 63, 68, 72, 75, 80], // 32 G#
[33, 52, 45, 57, 64, 69, 73, 76, 81], // 33 A
[34, 53, 46, 58, 65, 70, 74, 77, 82], // 34 A#
[35, 54, 47, 59, 66, 71, 75, 78, 83], // 35 B
[36, 55, 48, 60, 67, 72, 76, 79, 89], // 36 C
[37, 56, 49, 61, 68, 73, 77, 80, 90], // 37 C#
[38, 57, 50, 62, 69, 74, 78, 81, 91], // 38 D
[39, 58, 51, 63, 70, 75, 79, 82, 92], // 39 D#
[40, 59, 52, 64, 71, 76, 80, 83, 93], // 40 E
[41, 60, 53, 65, 72, 77, 81, 89, 94], // 41 F
[42, 61, 54, 66, 73, 78, 82, 90, 95], // 42 F#
[43, 62, 55, 67, 74, 79, 83, 91, 79], // 43 G
[44, 63, 56, 68, 75, 80, 89, 92, 80], // 44 G#
[45, 64, 57, 69, 76, 81, 90, 93, 81], // 45 A
[46, 65, 58, 70, 77, 82, 91, 94, 82], // 46 A#
[47, 66, 59, 71, 78, 83, 92, 95, 83], // 47 B
[48, 67, 60, 72, 79, 89, 93, 79, 89], // 48 C
[49, 68, 61, 73, 80, 90, 94, 80, 90], // 49 C#
[50, 69, 62, 74, 81, 91, 95, 81, 91], // 50 D
[51, 70, 63, 75, 82, 92, 79, 82, 92], // 51 D#
[52, 71, 64, 76, 83, 93, 80, 83, 93], // 52 E
[53, 72, 65, 77, 89, 94, 81, 89, 94], // 53 F
[54, 73, 66, 78, 90, 95, 82, 90, 95], // 54 F#
[55, 74, 67, 79, 91, 79, 83, 91, 79], // 55 G
[56, 75, 68, 80, 92, 80, 89, 92, 80], // 56 G#
[57, 76, 69, 81, 93, 81, 90, 93, 81], // 57 A
[58, 77, 70, 82, 94, 82, 91, 94, 82], // 58 A#
[59, 78, 71, 83, 95, 83, 92, 95, 83], // 59 B
[60, 79, 72, 89, 79, 89, 93, 79, 89], // 60 C
];


Prob­a­bly (well… def­i­nite­ly), not the most ef­fi­cient way to do this, but a close cor­re­spon­dence to the re­al thing. Now, we al­so need to store which keys (or ped­als) are pressed, ig­nor­ing things like con­tact bounce or the de­lay be­tween dif­fer­ent cir­cuits clos­ing for the time be­ing, and ad­just our MI­DI pro­cess­ing cor­re­spond­ing­ly. Haven't de­cid­ed which con­trol changes to use for which draw­bar yet, so we'll skip those. The man­u­als and ped­al­board will use one MI­DI chan­nel each.

pub struct TonewheelOrgan {
/* ... */
// replacing active_notes:
upper_active_notes: [bool; MANUAL_KEYS],
lower_active_notes: [bool; MANUAL_KEYS],
pedal_active_notes: [bool; PEDAL_KEYS],
}

impl TonewheelOrgan {
/* ... */

pub fn process_midi_message(&mut self, msg: &[u8]) {
let TonewheelOrgan {
upper_active_notes,
current_upper_preset,
lower_active_notes,
current_lower_preset,
pedal_active_notes,
..
} = self;
// use wmidi to parse msg
match MidiMessage::try_from(msg) {
Ok(MidiMessage::NoteOn(channel, note, _velocity)) => {
let note = note as u8;
// preset key range: C3 (24) to B3 (35)
if (24..36).contains(&note) {
match channel {
Channel::Ch1 => {
*current_upper_preset = note - 24
}
Channel::Ch2 => {
*current_lower_preset = note - 24
}
_ => {}
}
}
// manual range: C4 (36) to C9 (96)
if (36..=96).contains(&note) {
match channel {
Channel::Ch1 => {
upper_active_notes[(note - 36) as usize] =
true
}
Channel::Ch2 => {
lower_active_notes[(note - 36) as usize] =
true
}
_ => {}
}
}
}
Ok(MidiMessage::NoteOff(channel, note, _velocity)) => {
let note = note as u8;
// manual range: C4 (36) to C9 (96)
if (36..=96).contains(&note) {
match channel {
Channel::Ch1 => {
upper_active_notes[(note - 36) as usize] =
false
}
Channel::Ch2 => {
lower_active_notes[(note - 36) as usize] =
false
}
_ => {}
}
}
}
Ok(MidiMessage::Reset) => {
// deactivate all notes on reset
*upper_active_notes = [false; MANUAL_KEYS];
*lower_active_notes = [false; MANUAL_KEYS];
*pedal_active_notes = [false; PEDAL_KEYS];
}
_ => {}
}
}

/* ... */
}


You may have no­ticed that we switched away from us­ing an ar­ray of u8x4 for the ac­tive notes. The rea­son for that is the some­what chaot­ic in­dex­ing and most SIMD in­struc­tion sets not sup­port­ing any form of in­dex­ing (gath­er or scat­ter op­er­a­tions). Fi­nal­ly, we need to up­date our render_sample func­tion:

impl TonewheelOrgan {
/* ... */

pub fn render_sample(&mut self) -> f32 {
// generate tone wheel base signals
let mut signals = MaybeUninit::uninit_array();
self.generate_base_signals(&mut signals);
let signals =
unsafe { MaybeUninit::array_assume_init(signals) };

// cast f32x4 array to f32 array (can we do this without
// unsafe?)
let signals_scalar: &[f32; 4 * ROUNDED_TONE_WHEEL_CHUNKS] =
unsafe { transmute(&signals) };

let mut upper_partial_signals = [0.0; 9];
let mut lower_partial_signals = [0.0; 9];
for (upper_active, lower_active, generators) in itertools::izip!(
self.upper_active_notes.iter(),
self.lower_active_notes.iter(),
MANUAL_KEY_GENERATORS
) {
// convert bools to bit masks
(*upper_active as u32).wrapping_neg();
(*lower_active as u32).wrapping_neg();
for (upper_signal, lower_signal, generator) in itertools::izip!(
upper_partial_signals.iter_mut(),
lower_partial_signals.iter_mut(),
generators
) {
let generator =
unsafe { *signals_scalar.get_unchecked(generator) };
*upper_signal += f32::from_bits(
);
*lower_signal += f32::from_bits(
);
}
}

// look up drawbar gains using unsafe get_unchecked to prevent
// bounds checks the optimizer doesn't manage to remove
let upper_gains = {
let mut upper_gains = [0.0; 9];
for (gain, preset) in upper_gains.iter_mut().zip(unsafe {
*self
.upper_manual_presets
.get_unchecked(self.current_upper_preset as usize)
}) {
*gain = unsafe {
*DRAWBAR_GAINS.get_unchecked(preset as usize)
};
}
upper_gains
};
let lower_gains = {
let mut lower_gains = [0.0; 9];
for (gain, preset) in lower_gains.iter_mut().zip(unsafe {
*self
.lower_manual_presets
.get_unchecked(self.current_lower_preset as usize)
}) {
*gain = unsafe {
*DRAWBAR_GAINS.get_unchecked(preset as usize)
};
}
lower_gains
};

// apply drawbar gains and sum signals
let upper_signal = upper_partial_signals
.into_iter()
.zip(upper_gains)
.map(|(partial, gain)| partial * gain)
.sum::<f32>();
let lower_signal = lower_partial_signals
.into_iter()
.zip(lower_gains)
.map(|(partial, gain)| partial * gain)
.sum::<f32>();
let signal = 0.05 * (upper_signal + lower_signal);

// soft clipping
let signal = signal.max(-1.0).min(1.0);
let signal = 1.5 * signal - 0.5 * signal.powi(3);

signal
}

/* ... */
}


You may no­tice that we still haven't added ped­al sup­port here and that there are a num­ber of unsafe op­ti­miza­tions al­ready. Fur­ther­more, in­stead of sep­a­rate­ly com­put­ing the up­per and low­er man­u­als sep­a­rate­ly, we have a sin­gle loop that goes through both at once. Isn't pre­ma­ture op­ti­miza­tion the root of all evil?

I orig­i­nal­ly didn't ex­plic­it­ly re­move the bounds checks and had two sep­a­rate loops and ran in­to the Xeno­mai watch­dog timer be­cause the re­sult­ing render func­tion was too slow. And that's with­out adding the ped­als! Merg­ing the loops helps, be­cause few­er in­di­rect lookups are nec­es­sary.

To get a bet­ter idea of how much of our time bud­get of 22.7 μs we have left over, let's bench­mark the render_sample func­tion, up­dat­ing tonewheel-organ/Cargo.toml cor­re­spond­ing­ly:

use criterion::{
black_box, criterion_group, criterion_main, Criterion,
};
use tonewheel_organ::*;

fn criterion_benchmark(c: &mut Criterion) {
let mut organ = TonewheelOrgan::new(44_100.0);
c.bench_function("render_sample", |b| {
b.iter(|| TonewheelOrgan::render_sample(black_box(&mut organ)))
});
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);


At this point, I ac­tu­al­ly ran in­to an is­sue with my cross com­pi­la­tion tool chain that I didn't en­counter be­fore:

target/armv7-unknown-linux-gnueabihf/release/deps/render_sample-eac071ee5aed3608: /lib/arm-linux-gnueabihf/libc.so.6: version GLIBC_2.25' not found (required by target/armv7-unknown-linux-gnueabihf/release/deps/render_sample-eac071ee5aed3608)
error: bench failed


This means that the cross-com­pil­er is link­ing to a new­er ver­sion of libc.so (or cor­re­spond­ing stubs) than is used on the tar­get sys­tem. Af­ter down­grad­ing to the Linaro GCC 6.3 tool chain, the bench­mark built and ran. I al­so went ahead and up­dat­ed the first ar­ti­cle in this se­ries in which we set up the cross-com­pi­la­tion en­vi­ron­ment. So let's run that bench­mark now.

render_sample           time:   [28.470 us 28.560 us 28.679 us]
Found 7 outliers among 100 measurements (7.00%)
1 (1.00%) low mild
6 (6.00%) high severe
`

Whoops. That's not good. We have about −5.9 μs left over in our time bud­get. Why did it work at all? Prob­a­bly on­ly due to the bet­ter cache and op­ti­miza­tion be­hav­ior of hav­ing a block size greater than one in the main bi­na­ry! So guess what we'll be do­ing next time: more SIMD op­ti­miza­tion! But to not leave you hang­ing, here's a quick sound sam­ple:

The first half is just the out­put of the Bela —with notch fil­ters at 1.5 kHz, 3 kHz, 4.5 kHz, etc. due to some an­noy­ing buzz I haven't man­aged to get rid of, even us­ing a pro­fes­sion­al sound card. Maybe it's a Bela is­sue, or I have some­thing in my room emit­ting elec­tro­mag­net­ic in­ter­fer­ence at 1.5 kHz. For the sec­ond half, I added amp and ro­tary speak­er sim­u­la­tions us­ing a VST plug­in.

As usu­al, feel free to fol­low me and send me a DM on Twit­ter if you have any ques­tions or com­ments. I'm al­so ac­tive on the Bela fo­rum.