use crate::mixer::NUM_CHANNELS;
const PITCH_CLASSES: usize = 12;
pub const FREQ_NO_CHANGE: [u8; 3] = [0x7F, 0x7F, 0x7F];
pub fn freq_word_to_cents_offset(word: [u8; 3], key: u8) -> Option<f32> {
if word == FREQ_NO_CHANGE {
return None;
}
let semitone = (word[0] & 0x7F) as i32;
let fraction14 = (((word[1] & 0x7F) as u32) << 7) | ((word[2] & 0x7F) as u32);
let target_semitones = semitone as f32 + (fraction14 as f32) / 16384.0;
Some((target_semitones - key as f32) * 100.0)
}
pub fn scale_octave_1byte_to_cents(value: u8) -> f32 {
((value & 0x7F) as i32 - 64) as f32
}
pub fn scale_octave_2byte_to_cents(msb: u8, lsb: u8) -> f32 {
let raw = (((msb & 0x7F) as i32) << 7) | ((lsb & 0x7F) as i32);
(raw - 0x2000) as f32 * 200.0 / 16384.0
}
pub fn scale_octave_channel_mask(ff: u8, gg: u8, hh: u8) -> u16 {
let mut mask = 0u16;
for bit in 0..7 {
if hh & (1 << bit) != 0 {
mask |= 1 << bit;
}
}
for bit in 0..7 {
if gg & (1 << bit) != 0 {
mask |= 1 << (bit + 7);
}
}
for bit in 0..2 {
if ff & (1 << bit) != 0 {
mask |= 1 << (bit + 14);
}
}
mask
}
#[derive(Clone, Debug)]
pub struct TuningTable {
key_offsets: [f32; 128],
scale_octave: [[f32; PITCH_CLASSES]; NUM_CHANNELS],
}
impl Default for TuningTable {
fn default() -> Self {
Self {
key_offsets: [0.0; 128],
scale_octave: [[0.0; PITCH_CLASSES]; NUM_CHANNELS],
}
}
}
impl TuningTable {
pub fn new() -> Self {
Self::default()
}
pub fn offset_cents(&self, channel: u8, key: u8) -> f32 {
let k = key as usize;
let key_off = if k < 128 { self.key_offsets[k] } else { 0.0 };
let ch = channel as usize % NUM_CHANNELS;
let pc = (key % 12) as usize;
key_off + self.scale_octave[ch][pc]
}
pub fn set_key_freq_word(&mut self, key: u8, word: [u8; 3]) {
if (key as usize) >= 128 {
return;
}
if let Some(cents) = freq_word_to_cents_offset(word, key) {
self.key_offsets[key as usize] = cents;
}
}
pub fn set_scale_octave(&mut self, channel: u8, pitch_class: usize, cents: f32) {
if pitch_class >= PITCH_CLASSES {
return;
}
let ch = channel as usize % NUM_CHANNELS;
self.scale_octave[ch][pitch_class] = cents;
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn freq_word_equal_temperament_examples_are_zero_offset() {
for key in [0u8, 12, 60, 61, 120, 127] {
let off = freq_word_to_cents_offset([key, 0x00, 0x00], key).unwrap();
assert!(off.abs() < 1e-3, "key {key} offset {off}");
}
}
#[test]
fn freq_word_no_change_is_none() {
assert!(freq_word_to_cents_offset(FREQ_NO_CHANGE, 60).is_none());
}
#[test]
fn freq_word_one_lsb_is_about_006_cents() {
let off = freq_word_to_cents_offset([0x00, 0x00, 0x01], 0).unwrap();
assert!((off - 100.0 / 16384.0).abs() < 1e-6, "off {off}");
}
#[test]
fn freq_word_addressed_to_lower_key_is_positive_semitones() {
let off = freq_word_to_cents_offset([0x3D, 0x00, 0x00], 60).unwrap();
assert!((off - 100.0).abs() < 1e-3, "off {off}");
}
#[test]
fn freq_word_half_fraction_is_50_cents() {
let off = freq_word_to_cents_offset([0x3C, 0x40, 0x00], 60).unwrap();
assert!((off - 50.0).abs() < 1e-3, "off {off}");
}
#[test]
fn scale_octave_1byte_endpoints() {
assert_eq!(scale_octave_1byte_to_cents(0x00), -64.0);
assert_eq!(scale_octave_1byte_to_cents(0x40), 0.0);
assert_eq!(scale_octave_1byte_to_cents(0x7F), 63.0);
}
#[test]
fn scale_octave_2byte_endpoints() {
assert!((scale_octave_2byte_to_cents(0x00, 0x00) - -100.0).abs() < 1e-3);
assert!(scale_octave_2byte_to_cents(0x40, 0x00).abs() < 1e-3);
let top = scale_octave_2byte_to_cents(0x7F, 0x7F);
assert!((top - 99.988).abs() < 0.05, "top {top}");
}
#[test]
fn channel_mask_low_high_bits() {
assert_eq!(scale_octave_channel_mask(0, 0, 0x01) & 1, 1);
assert_eq!(scale_octave_channel_mask(0, 0x01, 0) >> 7 & 1, 1);
assert_eq!(scale_octave_channel_mask(0x02, 0, 0) >> 15 & 1, 1);
assert_eq!(scale_octave_channel_mask(0x7C, 0, 0), 0);
assert_eq!(scale_octave_channel_mask(0x03, 0x7F, 0x7F), 0xFFFF);
}
#[test]
fn table_default_is_zero_offset_everywhere() {
let t = TuningTable::new();
for ch in 0..16u8 {
for key in 0..128u8 {
assert_eq!(t.offset_cents(ch, key), 0.0);
}
}
}
#[test]
fn table_key_offset_sums_with_scale_octave() {
let mut t = TuningTable::new();
t.set_key_freq_word(60, [0x3C, 0x10, 0x00]); let expect_key = 100.0 * (0x800 as f32) / 16384.0; assert!((t.offset_cents(0, 60) - expect_key).abs() < 1e-3);
t.set_scale_octave(3, 0, -10.0);
assert!((t.offset_cents(3, 60) - (expect_key - 10.0)).abs() < 1e-3);
assert!((t.offset_cents(0, 60) - expect_key).abs() < 1e-3);
assert!((t.offset_cents(3, 72) - -10.0).abs() < 1e-3);
}
#[test]
fn table_reset_restores_equal_temperament() {
let mut t = TuningTable::new();
t.set_key_freq_word(64, [0x41, 0x00, 0x00]);
t.set_scale_octave(5, 4, 33.0);
t.reset();
assert_eq!(t.offset_cents(0, 64), 0.0);
assert_eq!(t.offset_cents(5, 64), 0.0);
}
#[test]
fn set_key_no_change_word_preserves_prior() {
let mut t = TuningTable::new();
t.set_key_freq_word(64, [0x41, 0x00, 0x00]); let before = t.offset_cents(0, 64);
t.set_key_freq_word(64, FREQ_NO_CHANGE); assert_eq!(t.offset_cents(0, 64), before);
}
}