Last active
April 11, 2025 17:12
-
-
Save randrews/f5c60a7e12c283e34a26aeb7dce02afc to your computer and use it in GitHub Desktop.
Generating a MIDI file of changes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "carillon" | |
version = "0.1.0" | |
edition = "2024" | |
[dependencies] | |
midi_file = "0.0.6" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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