Skip to content

Instantly share code, notes, and snippets.

@kolektiv
Last active December 6, 2023 12:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kolektiv/f707cc14cf0321009719fab5b65b4e1f to your computer and use it in GitHub Desktop.
Save kolektiv/f707cc14cf0321009719fab5b65b4e1f to your computer and use it in GitHub Desktop.
An incredibly hacky but passable midi clock...
use std::{
thread::{
self,
Builder,
},
time::Duration,
};
use midir::{
MidiOutput,
MidiOutputConnection,
};
use quanta::Clock;
use thread_priority::{
ThreadBuilderExt,
ThreadPriority,
};
// Timing (Nanosleep)
#[repr(C)]
#[derive(Copy, Clone)]
pub struct TimeVal {
sec: isize,
usec: isize,
}
extern "C" {
fn nanosleep(req: *const TimeVal, rem: usize) -> i32;
}
// Clock
const PPQN: u64 = 960;
const NANOS_PER_MINUTE: u64 = 60_000_000_000;
const START: [u8; 1] = [0xfa];
const PULSE: [u8; 1] = [0xf8];
fn clock(bpm: u64, mut connection: MidiOutputConnection) {
// Interval is the theoretical "exact" amount of time there should be between
// pulses in ns. The scaled interval is how much time we'll attempt
// to sleep for, based on needing some time to perform work "per tick". The
// pulse interval is how often a time clock pulse should be sent (so with a base
// PPQN of 24, it would be every pulse, with a base PPQN of 960, it would be
// every 40, etc.)
let interval = (NANOS_PER_MINUTE / bpm) / PPQN;
let scaled_interval = (interval as f64 * 0.8f64) as u64;
let pulse_interval = PPQN / 24u64;
// We'll attempt to sleep for the scaled interval time.
let clock = Clock::new();
let time = TimeVal {
sec: 0,
usec: scaled_interval as isize,
};
// Send a MIDI Clock start message.
connection.send(&START).expect("send start");
// Pulse counter and send are simply keeping track of the pulse interval, and
// whether to send on this pulse.
let mut pulse_counter = 1;
let mut pulse_send = false;
// We'll attempt to compensate slightly for the time the sending of data takes
// by keeping a rolling average of the last 10 durations in nanoseconds. The
// averaged time taken can be subtracted from the theoretical interval to give
// the time we should actually wait while spinning.
let mut last = 0u64;
let mut recent = [0u64, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let mut wait;
// Clock measurements are only used locally, but we can avoid allocation.
let mut start;
loop {
// Taking a starting clock measurement.
start = clock.raw();
// Sleep for the scaled interval time, which should leave a reasonable amount of
// time to do whatever work needs doing "per pulse".
unsafe {
nanosleep(&time, 0);
}
// Do whatever work needs doing in this section, in this case just keeping track
// of sending clock pulses.
// Work Start -----------------
if pulse_counter == pulse_interval {
pulse_send = true;
pulse_counter = 1;
} else {
pulse_counter += 1;
}
// Work End -------------------
// Calculate the rolling average of the recent times taken to send output, etc.
// and calculate a spin wait time based on the interval minus the average output
// time.
recent.rotate_right(1);
recent[0] = last;
wait = interval - (recent.iter().sum::<u64>() / recent.len() as u64);
// Spin loop until we're "at" the pulse time.
loop {
if clock.delta_as_nanos(start, clock.raw()) >= wait {
break;
}
}
// Time this operation to try and correct for the average time taken.
start = clock.raw();
if pulse_send {
connection.send(&PULSE).expect("send pulse");
pulse_send = false;
}
last = clock.delta_as_nanos(start, clock.raw());
}
}
// Main
fn main() {
let output = MidiOutput::new("test_output").expect("output");
let ports = output.ports();
let connection = output.connect(&ports[0], "output").expect("connection");
// Spawn the clock thread with high priority, and target BPM.
Builder::new()
.name(String::from("clock"))
.spawn_with_priority(ThreadPriority::Max, |result| match result {
Err(err) => panic!("{err:#?}"),
_ => clock(180, connection),
})
.expect("clock thread");
// For now we won't bother with actually controlling the thread or exiting
// gracefully, we'll just die after 40 seconds.
thread::sleep(Duration::from_secs(40));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment