Last active
December 6, 2023 12:57
-
-
Save kolektiv/f707cc14cf0321009719fab5b65b4e1f to your computer and use it in GitHub Desktop.
An incredibly hacky but passable midi clock...
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::{ | |
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