Skip to content

Instantly share code, notes, and snippets.

@randrews
Last active April 11, 2025 17:12
Show Gist options
  • Save randrews/f5c60a7e12c283e34a26aeb7dce02afc to your computer and use it in GitHub Desktop.
Save randrews/f5c60a7e12c283e34a26aeb7dce02afc to your computer and use it in GitHub Desktop.
Generating a MIDI file of changes
[package]
name = "carillon"
version = "0.1.0"
edition = "2024"
[dependencies]
midi_file = "0.0.6"
use std::fs;
use midi_file::core::{Channel, Clocks, DurationName, GeneralMidi, NoteNumber, Velocity};
use midi_file::file::{QuartersPerMinute, Track};
use midi_file::MidiFile;
// In plain hunt, each bell swaps with its successor, but we alternate
// on even / odd changes with leaving the lead bell alone. So, an even change:
// (1 2) (3 4) (5 6)
// and an odd change:
// 1 (2 3) (4 5) 6
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
}
// The MIDI note numbers of the bells. Traditionally, bells are tuned to a
// diatonic major scale:
// A5, G#5, F#5, E5, D5, C#5, B4, A4.
// Confusingly these are listed treble-first; lower numbers in change ringing
// mean higher pitches, the 1-bell is the treble and the 8-bell (or whatever)
// the tenor.
const NOTES: [u8; 8] = [81, 80, 78, 76, 74, 73, 71, 69];
fn main() {
let num_bells = 8;
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();
let mut change: Vec<_> = (1..num_bells + 1).collect();
for n in 0..num_bells * 2 {
println!("{:?}", change);
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();
}
change = plain_hunt(change, n)
}
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");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plain_hunt() {
assert_eq!(plain_hunt(vec![1, 2, 3, 4, 5, 6], 0),
vec![2, 1, 4, 3, 6, 5]);
assert_eq!(plain_hunt(vec![2, 1, 4, 3, 6, 5], 1),
vec![2, 4, 1, 6, 3, 5])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment