#![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, SYNC_HZ};
#[must_use]
#[allow(dead_code)]
pub(crate) fn encode_pd(mode: SstvMode, ycrcb: &[[u8; 3]]) -> Vec<f32> {
assert!(matches!(
mode,
SstvMode::Pd120 | SstvMode::Pd180 | SstvMode::Pd240
));
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);
assert_eq!(h % 2, 0);
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
};
for y_pair in 0..h / 2 {
tone.fill_to(SYNC_HZ, advance(&mut t, spec.sync_seconds));
tone.fill_to(PORCH_HZ, advance(&mut t, spec.porch_seconds));
for x in 0..w {
let lum = ycrcb[((y_pair * 2) * w + x) as usize][0];
tone.fill_to(lum_to_freq(lum), advance(&mut t, spec.pixel_seconds));
}
for x in 0..w {
let cr_a = ycrcb[((y_pair * 2) * w + x) as usize][1];
let cr_b = ycrcb[((y_pair * 2 + 1) * w + x) as usize][1];
let cr = u8::midpoint(cr_a, cr_b);
tone.fill_to(lum_to_freq(cr), advance(&mut t, spec.pixel_seconds));
}
for x in 0..w {
let cb_a = ycrcb[((y_pair * 2) * w + x) as usize][2];
let cb_b = ycrcb[((y_pair * 2 + 1) * w + x) as usize][2];
let cb = u8::midpoint(cb_a, cb_b);
tone.fill_to(lum_to_freq(cb), advance(&mut t, spec.pixel_seconds));
}
for x in 0..w {
let lum = ycrcb[((y_pair * 2 + 1) * w + x) as usize][0];
tone.fill_to(lum_to_freq(lum), advance(&mut t, spec.pixel_seconds));
}
}
tone.into_vec()
}
#[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_pd120_first_tone_is_sync_hz() {
let spec = for_mode(SstvMode::Pd120);
let img = vec![[128_u8, 128, 128]; (spec.line_pixels * spec.image_lines) as usize];
let audio = encode_pd(SstvMode::Pd120, &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,
"PD line starts with SYNC tone (p_sync={p_sync}, p_porch={p_porch})"
);
}
#[test]
fn encode_pd120_length_matches_radio_frames() {
let spec = for_mode(SstvMode::Pd120);
let img = vec![[0_u8; 3]; (spec.line_pixels * spec.image_lines) as usize];
let audio = encode_pd(SstvMode::Pd120, &img);
let radio_frames = f64::from(spec.image_lines) / 2.0;
let expected = (radio_frames
* 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,
"PD120 audio len {} ≉ {expected} (diff {})",
audio.len(),
diff
);
}
}