use crate::{Error, FrameRate, Result};
pub const BANDS: usize = 4;
pub const TONES_PER_BAND: usize = 16;
pub const BAND_GAP: usize = 2;
pub const TONE_COUNT: usize = BANDS * TONES_PER_BAND;
pub const MIN_FREQUENCY_HZ: f64 = 600.0;
pub const TONE_AMPLITUDE: f64 = 0.08;
pub const ID_MODULUS: u32 = 1 << 16;
pub const HOPS: [usize; BANDS] = [0, 5, 7, 11];
#[derive(Clone, Debug, PartialEq)]
pub struct ToneCode {
fps: FrameRate,
sample_rate: u32,
base_harmonic: u32,
}
impl ToneCode {
pub fn new(fps: FrameRate, sample_rate: u32) -> Result<Self> {
Self::with_base_harmonic(fps, sample_rate, Self::default_base_harmonic(fps))
}
pub fn default_base_harmonic(fps: FrameRate) -> u32 {
(MIN_FREQUENCY_HZ / fps.as_f64()).ceil() as u32
}
pub fn with_base_harmonic(
fps: FrameRate,
sample_rate: u32,
base_harmonic: u32,
) -> Result<Self> {
if base_harmonic == 0 {
return Err(Error::InvalidOption(
"tone base harmonic must be positive".into(),
));
}
let code = ToneCode {
fps,
sample_rate,
base_harmonic,
};
let max = code.frequency(BANDS - 1, TONES_PER_BAND - 1);
if max > 0.45 * f64::from(sample_rate) {
return Err(Error::InvalidOption(format!(
"{} fps needs tones up to {max:.0} Hz, too high for a {sample_rate} Hz sample rate",
fps.label()
)));
}
Ok(code)
}
pub fn fps(&self) -> FrameRate {
self.fps
}
pub fn base_harmonic(&self) -> u32 {
self.base_harmonic
}
pub fn descriptor(&self) -> String {
format!("K{}", self.base_harmonic)
}
pub fn parse_descriptor(text: &str) -> Result<u32> {
let unsupported = || Error::Unsupported(format!("audio code `{text}`"));
let k = text.strip_prefix('K').ok_or_else(unsupported)?;
k.parse().ok().filter(|&k| k > 0).ok_or_else(unsupported)
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn samples_per_frame(&self) -> f64 {
self.fps.samples_per_frame(self.sample_rate)
}
pub fn harmonic(&self, band: usize, nibble: usize) -> u32 {
self.base_harmonic + (band * (TONES_PER_BAND + BAND_GAP) + nibble) as u32
}
pub fn frequency(&self, band: usize, nibble: usize) -> f64 {
f64::from(self.harmonic(band, nibble)) * self.fps.as_f64()
}
pub fn nibbles(id: u16) -> [usize; BANDS] {
std::array::from_fn(|b| usize::from((id >> (4 * b)) & 0xF))
}
pub fn id_from_nibbles(nibbles: [usize; BANDS]) -> u16 {
nibbles
.iter()
.enumerate()
.fold(0, |id, (b, &n)| id | ((n as u16 & 0xF) << (4 * b)))
}
pub fn tone(band: usize, id: u16) -> usize {
let nibbles = Self::nibbles(id);
(nibbles[band] + HOPS[band] * nibbles[0]) % TONES_PER_BAND
}
pub fn nibble(band: usize, tone: usize, low: usize) -> usize {
(tone + TONES_PER_BAND * TONES_PER_BAND - HOPS[band] * low % TONES_PER_BAND)
% TONES_PER_BAND
}
pub fn tones(id: u16) -> [usize; BANDS] {
std::array::from_fn(|b| Self::tone(b, id))
}
pub fn id_from_tones(tones: [usize; BANDS]) -> u16 {
let low = tones[0];
Self::id_from_nibbles(std::array::from_fn(|b| Self::nibble(b, tones[b], low)))
}
pub fn guard_harmonics(&self) -> Vec<u32> {
(0..BANDS - 1)
.flat_map(|b| {
let top = self.harmonic(b, TONES_PER_BAND - 1);
(1..=BAND_GAP as u32).map(move |g| top + g)
})
.collect()
}
pub fn signal_at(&self, t: f64) -> f64 {
(0..BANDS)
.filter_map(|band| self.band_phase(band, t))
.map(|phase| TONE_AMPLITUDE * phase.sin())
.sum()
}
pub fn band_phase(&self, band: usize, t: f64) -> Option<f64> {
if t < 0.0 {
return None;
}
let position = t * self.fps.as_f64();
let frame = position.floor();
let frac = position - frame;
let id = (frame as u64 % u64::from(ID_MODULUS)) as u16;
let k = f64::from(self.harmonic(band, Self::tone(band, id)));
Some(std::f64::consts::TAU * k * frac)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nibbles_roundtrip() {
for id in [0u16, 1, 0xF, 0x10, 0xABCD, 0xFFFF] {
assert_eq!(ToneCode::id_from_nibbles(ToneCode::nibbles(id)), id);
}
assert_eq!(ToneCode::nibbles(0x1234), [4, 3, 2, 1]);
}
#[test]
fn tones_hop_every_frame_and_decode() {
for id in 0..=u16::MAX {
let tones = ToneCode::tones(id);
assert_eq!(ToneCode::id_from_tones(tones), id, "id {id}");
if id > 0 {
let before = ToneCode::tones(id - 1);
for b in 0..BANDS {
assert_ne!(tones[b], before[b], "band {b} held from {} to {id}", id - 1);
}
}
}
for b in 0..BANDS {
let mut seen = [false; TONES_PER_BAND];
for id in 0x1230..0x1240u16 {
seen[ToneCode::tone(b, id)] = true;
}
assert!(seen.iter().all(|&s| s), "band {b}");
}
}
#[test]
fn frequency_plan() {
let code = ToneCode::new("30".parse().unwrap(), 48000).unwrap();
assert_eq!(code.frequency(0, 0), 600.0);
assert_eq!(code.frequency(3, 15), 2670.0);
let code = ToneCode::new("60".parse().unwrap(), 48000).unwrap();
assert_eq!(code.frequency(3, 15), 4740.0);
let code = ToneCode::new("120".parse().unwrap(), 48000).unwrap();
assert_eq!(code.frequency(3, 15), 8880.0);
assert!(ToneCode::new("120".parse().unwrap(), 16000).is_err());
}
#[test]
fn descriptor_roundtrip() {
let code = ToneCode::new("29.97".parse().unwrap(), 48000).unwrap();
assert_eq!(code.descriptor(), "K21");
assert_eq!(ToneCode::parse_descriptor("K21").unwrap(), 21);
assert!(ToneCode::parse_descriptor("T4X16G2K21").is_err());
assert!(ToneCode::parse_descriptor("K0").is_err());
assert!(ToneCode::parse_descriptor("K").is_err());
}
#[test]
fn signal_is_continuous_at_frame_boundaries() {
let code = ToneCode::new("29.97".parse().unwrap(), 48000).unwrap();
for frame in [1i64, 2, 100, 1000] {
let t = code.fps().frame_time(frame);
assert!(code.signal_at(t - 1e-9).abs() < 1e-4);
assert!(code.signal_at(t + 1e-9).abs() < 1e-4);
}
assert_eq!(code.signal_at(-0.5), 0.0);
}
}