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.

Photo of a Hammond B3 organ taken at Musical Instrument Museum (Phoenix, AZ), showing both manuals, drawbars, and pedals
bo­bis­trav­el­ing, CC BY 2.0 <https://creativecommons.org/licenses/by/2.0>, via Wiki­me­dia Com­mons, cropped

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 2\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
            let upper_active_mask =
                (*upper_active as u32).wrapping_neg();
            let lower_active_mask =
                (*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(
                    generator.to_bits() & upper_active_mask,
                );
                *lower_signal += f32::from_bits(
                    generator.to_bits() & lower_active_mask,
                );
            }
        }

        // 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 Mastodon if you have any ques­tions or com­ments. I'm al­so ac­tive on the Bela fo­rum.