#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
use crate::modespec::SstvMode;
use crate::resample::WORKING_SAMPLE_RATE_HZ;
use crate::test_tone::{lum_to_freq, ToneWriter, PORCH_HZ, SEPTR_HZ, SYNC_HZ};
#[must_use]
#[allow(dead_code)]
pub(crate) fn encode_robot(mode: SstvMode, ycrcb: &[[u8; 3]]) -> Vec<f32> {
let spec = crate::modespec::for_mode(mode);
let w = spec.line_pixels;
let h = spec.image_lines;
assert_eq!(ycrcb.len() as u32, w * h);
let sr = f64::from(WORKING_SAMPLE_RATE_HZ);
let mut tone = ToneWriter::new();
let mut t = 0.0_f64;
let advance = |t: &mut f64, secs: f64| -> usize {
*t += secs;
(*t * sr).round() as usize
};
match mode {
SstvMode::Robot72 => encode_r72(&mut tone, &mut t, advance, &spec, ycrcb),
SstvMode::Robot24 | SstvMode::Robot36 => {
encode_r36_or_r24(&mut tone, &mut t, advance, &spec, ycrcb);
}
_ => unreachable!("encode_robot called with non-Robot mode {mode:?}"),
}
tone.into_vec()
}
fn encode_r72(
tone: &mut ToneWriter,
t: &mut f64,
mut advance: impl FnMut(&mut f64, f64) -> usize,
spec: &crate::modespec::ModeSpec,
ycrcb: &[[u8; 3]],
) {
let w = spec.line_pixels;
let h = spec.image_lines;
for y in 0..h {
tone.fill_to(SYNC_HZ, advance(t, spec.sync_seconds));
tone.fill_to(PORCH_HZ, advance(t, spec.porch_seconds));
for x in 0..w {
let lum = ycrcb[(y * w + x) as usize][0];
tone.fill_to(lum_to_freq(lum), advance(t, spec.pixel_seconds));
}
tone.fill_to(SEPTR_HZ, advance(t, spec.septr_seconds));
for x in 0..w {
let cr = ycrcb[(y * w + x) as usize][1];
tone.fill_to(lum_to_freq(cr), advance(t, spec.pixel_seconds));
}
tone.fill_to(SEPTR_HZ, advance(t, spec.septr_seconds));
for x in 0..w {
let cb = ycrcb[(y * w + x) as usize][2];
tone.fill_to(lum_to_freq(cb), advance(t, spec.pixel_seconds));
}
let line_end_target = f64::from(y + 1) * spec.line_seconds;
let pad_secs = line_end_target - *t;
if pad_secs > 0.0 {
tone.fill_to(PORCH_HZ, advance(t, pad_secs));
}
}
}
fn encode_r36_or_r24(
tone: &mut ToneWriter,
t: &mut f64,
mut advance: impl FnMut(&mut f64, f64) -> usize,
spec: &crate::modespec::ModeSpec,
ycrcb: &[[u8; 3]],
) {
let w = spec.line_pixels;
let h = spec.image_lines;
for y in 0..h {
tone.fill_to(SYNC_HZ, advance(t, spec.sync_seconds));
tone.fill_to(PORCH_HZ, advance(t, spec.porch_seconds));
for x in 0..w {
let lum = ycrcb[(y * w + x) as usize][0];
tone.fill_to(lum_to_freq(lum), advance(t, spec.pixel_seconds * 2.0));
}
tone.fill_to(SEPTR_HZ, advance(t, spec.septr_seconds));
let chroma_idx = if y % 2 == 0 { 1_usize } else { 2_usize };
for x in 0..w {
let chroma = ycrcb[(y * w + x) as usize][chroma_idx];
tone.fill_to(lum_to_freq(chroma), advance(t, spec.pixel_seconds));
}
}
}
#[cfg(test)]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
mod tests {
use super::*;
use crate::modespec::{for_mode, SstvMode};
#[test]
fn encode_robot72_first_tone_is_sync_hz() {
let spec = for_mode(SstvMode::Robot72);
let img = vec![[128_u8, 128, 128]; (spec.line_pixels * spec.image_lines) as usize];
let audio = encode_robot(SstvMode::Robot72, &img);
let sync_samples =
(spec.sync_seconds * f64::from(crate::resample::WORKING_SAMPLE_RATE_HZ)) as usize;
assert!(audio.len() >= sync_samples, "audio too short");
let p_sync = crate::dsp::goertzel_power(&audio[..sync_samples], crate::test_tone::SYNC_HZ);
let p_porch =
crate::dsp::goertzel_power(&audio[..sync_samples], crate::test_tone::PORCH_HZ);
assert!(
p_sync > 10.0 * p_porch,
"Robot72 line starts with SYNC tone (p_sync={p_sync}, p_porch={p_porch})"
);
}
#[test]
fn encode_robot72_length_matches_radio_lines() {
let spec = for_mode(SstvMode::Robot72);
let img = vec![[0_u8; 3]; (spec.line_pixels * spec.image_lines) as usize];
let audio = encode_robot(SstvMode::Robot72, &img);
let radio_lines = f64::from(spec.image_lines);
let expected = (radio_lines
* spec.line_seconds
* f64::from(crate::resample::WORKING_SAMPLE_RATE_HZ)) as usize;
let diff = (audio.len() as i64 - expected as i64).abs();
assert!(
diff < 64,
"Robot72 audio len {} ≉ {expected} (diff {})",
audio.len(),
diff
);
}
}