Plain Hunting in Rust


I came across an interesting thing the other day and decided to play with it some. Here are the results of that:

Churches have bells. Some churches have carillons of several bells, 6 or 8 or 12, tuned to different notes. If you pull a rope and start the bell swinging, it'll ring over and over at a roughly constant period. If you put one person on each rope, you can ring each of the bells in a sequence. So for example with 8 bells, you can ring (1, 2, 3, 4, 5, 6, 7, 8) over and over again. The 1 bell is the smallest and highest pitched, and is called the "treble;" the 8 bell is called the "tenor."

The ringers weigh much less than the bells, but with mechanical advantage and good balance on the carriages they can tug on the ropes to alter the period a little bit each time. The periods are still mostly constant, but you can, say, swap a bell's position in the order with the one before it or the one after it. You can't alter the period more than that.

Suppose you have some rule by which you alter the bells' positions in the order each time, and play some or all of the permutations that way: this is called "ringing the changes," and is a unique form of music dating back to medieval times. It's pretty much entirely determined by the physics of a humongous brass bell swinging in a carriage; you can't play melodies or anything like that, the artistry comes from the mathematics of the rules governing how one permutation (one "change") evolves into the next one.

There are many rules to generate changes, and the simplest one is called "Plain Hunt." I urge you to look at this page and try to work out the rules for yourself before reading further. The page itself is written from the perspective of teaching one ringer what to do; the overall rule isn't obvious but it's satisfying to work out.


Every change, pairs of adjacent bells are swapped, and depending on whether the change is odd- or even-numbered, the lead bell either swaps or doesn't. So, start with bells in a round:
1 2 3 4 5 6
Because this is the starting change, it's even-numbered (0), so we swap all adjacent pairs:
(1 2) (3 4) (5 6) --> 2 1 4 3 6 5
The lead bell is now 2. The next change is odd-numbered, so we leave the lead bell in place and start our swapping at an offset of 1:
2 (1 4) (3 6) 5 --> 2 4 1 6 3 5
Each change is different from all the others, and after 2n iterations we'll be back to a round, 1 2 3 4 5 6. There are much more complicated rules that take longer to cycle, but many of them incorporate sequences of plain hunt in them, so this is a good place to start.


Here's a simple implementation of plain hunt in Rust:

fn plain_hunt(change: Vec<u32>, n: u32) -> Vec<u32> {
    let mut next_change = change.clone();
    fn swap(change: &mut [u32], i: usize) {
        (change[i+1], change[i]) = (change[i], change[i+1])
    }

    let start = (n % 2) as usize;

    for i in (start..change.len() - 1).step_by(2) {
        swap(&mut next_change, i)
    }

    next_change
}

Given a change (as a Vec<u32>) and which change it is in the sequence, return the succeeding change. We can trivially print out all the changes:

fn main() {
    let num_bells = 8;

    let mut change: Vec<_> = (1..num_bells + 1).collect();
    for n in 0..num_bells * 2 {
        println!("{:?}", change);
        change = plain_hunt(change, n)
    }
}
[1, 2, 3, 4, 5, 6, 7, 8]
[2, 1, 4, 3, 6, 5, 8, 7]
[2, 4, 1, 6, 3, 8, 5, 7]
[4, 2, 6, 1, 8, 3, 7, 5]
[4, 6, 2, 8, 1, 7, 3, 5]
[6, 4, 8, 2, 7, 1, 5, 3]
[6, 8, 4, 7, 2, 5, 1, 3]
[8, 6, 7, 4, 5, 2, 3, 1]
[8, 7, 6, 5, 4, 3, 2, 1]
[7, 8, 5, 6, 3, 4, 1, 2]
[7, 5, 8, 3, 6, 1, 4, 2]
[5, 7, 3, 8, 1, 6, 2, 4]
[5, 3, 7, 1, 8, 2, 6, 4]
[3, 5, 1, 7, 2, 8, 4, 6]
[3, 1, 5, 2, 7, 4, 8, 6]
[1, 3, 2, 5, 4, 7, 6, 8]

Note that this is obviously not all the changes; there are 8! possible changes on 8 bells and plain hunt will only generate 16 (2 * 8) of them. A "full peal" on 8 bells would be 40,320 changes. A full peal on seven bells takes about three hours to play.


But listing the changes isn't much fun; I'd rather hear them! So let's see if we can't do something about that.

The obvious answer here is to use MIDI, because we can just write out the notes to play, which we already have done. I went down somewhat of a rabbit hole last night trying to write a MIDI file by hand before deciding to just use a crate. So using that crate, let's set up a file:

let mut mfile = MidiFile::new();
let mut track = Track::default();

// General MIDI doesn't have a church bell instrument!
// But we have tubular bells which is close enough:
track.set_general_midi(Channel::new(0), GeneralMidi::TubularBells).unwrap();
track.push_time_signature(0, 4, DurationName::Quarter, Clocks::Quarter).unwrap();
track.push_tempo(0, QuartersPerMinute::new(120)).unwrap();

// ... write notes ...

mfile.push_track(track).unwrap();
let mut midi = vec![];
mfile.write(&mut midi).expect("Unable to assemble MIDI bytes");
fs::write("plain_hunt.mid", midi).expect("Unable to write file");

Pretty straightforward so far. midi_file gives us a slice of bytes which we can write to a file. We build that slice of bytes by setting some parameters (tempo, time signature, etc) and then appending notes to a track. What notes?

// A5, G#5, F#5, E5, D5, C#5, B4, A4.
const NOTES: [u8; 8] = [81, 80, 78, 76, 74, 73, 71, 69];

Bells are traditionally tuned to a diatonic major scale. If we start that scale at A4 (concert A, A above middle C, 440 Hz, MIDI note number 69) then we get those note numbers for each of the 8 bells (we could continue the scale upward from A5 or downward from A4 but 8 bells is enough for this demo).

Note that confusingly, as the bell numbers go up, the pitches go down; the tenor is bell 8 and the treble is bell 1. So this array probably looks reversed from what you'd expect in other musical programming.

Now we need to actually write the note messages, which look like this:

let ch = Channel::new(0);
let sv = Velocity::new(200); // Strike velocity; arbitrary
let rv = Velocity::new(8); // Release velocity

for (n, &bell) in change.iter().enumerate() {
    let note = NOTES[bell as usize - 1]; // Bell numbers start at 1 for readability

    // How long should we delay before note start?
    // If it's the lead bell, pause a beat to separate the changes:
    let pause = if n == 0 { 512 } else { 0 };
    track.push_note_on(pause, ch, NoteNumber::new(note), sv).unwrap();
    track.push_note_off(512, ch, NoteNumber::new(note), rv).unwrap();
}

To let listeners tell the changes apart, we'll rest for a beat before the lead bell of each change.


If you run this then you get just over a kilobyte of MIDI, which you can listen to here. Enjoy!