Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b0c0ed81 |
@@ -20,14 +20,26 @@ fn main() {
|
|||||||
for x in 0..w {
|
for x in 0..w {
|
||||||
let at = (y * w + x) * 3;
|
let at = (y * w + x) * 3;
|
||||||
let (r, g, b) = if y < h / 3 {
|
let (r, g, b) = if y < h / 3 {
|
||||||
[(255u8, 255u8, 255u8), (255, 255, 0), (0, 255, 255), (0, 255, 0),
|
[
|
||||||
(255, 0, 255), (255, 0, 0), (0, 0, 255), (0, 0, 0)][x * 8 / w]
|
(255u8, 255u8, 255u8),
|
||||||
|
(255, 255, 0),
|
||||||
|
(0, 255, 255),
|
||||||
|
(0, 255, 0),
|
||||||
|
(255, 0, 255),
|
||||||
|
(255, 0, 0),
|
||||||
|
(0, 0, 255),
|
||||||
|
(0, 0, 0),
|
||||||
|
][x * 8 / w]
|
||||||
} else if y < 2 * h / 3 {
|
} else if y < 2 * h / 3 {
|
||||||
let t = (x * 255 / w) as u8;
|
let t = (x * 255 / w) as u8;
|
||||||
(t, 255 - t, ((y * 255) / h) as u8)
|
(t, 255 - t, ((y * 255) / h) as u8)
|
||||||
} else {
|
} else {
|
||||||
// Diagonal stripes: a line-timing error shows up as a kink.
|
// Diagonal stripes: a line-timing error shows up as a kink.
|
||||||
if ((x + y) / 16) % 2 == 0 { (240, 240, 40) } else { (20, 20, 90) }
|
if ((x + y) / 16) % 2 == 0 {
|
||||||
|
(240, 240, 40)
|
||||||
|
} else {
|
||||||
|
(20, 20, 90)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
rgb[at] = r;
|
rgb[at] = r;
|
||||||
rgb[at + 1] = g;
|
rgb[at + 1] = g;
|
||||||
@@ -39,9 +51,17 @@ fn main() {
|
|||||||
for y in 0..h {
|
for y in 0..h {
|
||||||
sent.put_row(y, &rgb[y * w * 3..(y + 1) * w * 3]);
|
sent.put_row(y, &rgb[y * w * 3..(y + 1) * w * 3]);
|
||||||
}
|
}
|
||||||
std::fs::write(format!("{dir}/{name}-sent.png"), sent.to_png().expect("png")).expect("write");
|
std::fs::write(
|
||||||
|
format!("{dir}/{name}-sent.png"),
|
||||||
|
sent.to_png().expect("png"),
|
||||||
|
)
|
||||||
|
.expect("write");
|
||||||
|
|
||||||
let frame = Frame { width: w, height: h, rgb: &rgb };
|
let frame = Frame {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
rgb: &rgb,
|
||||||
|
};
|
||||||
let mut audio = encode(mode, &frame, 48_000);
|
let mut audio = encode(mode, &frame, 48_000);
|
||||||
audio.extend(std::iter::repeat_n(0.0, 4800));
|
audio.extend(std::iter::repeat_n(0.0, 4800));
|
||||||
let mut decoder = SstvDecoder::new(48_000, SstvConfig::default());
|
let mut decoder = SstvDecoder::new(48_000, SstvConfig::default());
|
||||||
@@ -58,7 +78,14 @@ fn main() {
|
|||||||
for y in 0..h {
|
for y in 0..h {
|
||||||
canvas.put_row(y, &image.rgb[y * w * 3..(y + 1) * w * 3]);
|
canvas.put_row(y, &image.rgb[y * w * 3..(y + 1) * w * 3]);
|
||||||
}
|
}
|
||||||
std::fs::write(format!("{dir}/{name}-decoded.png"), canvas.to_png().expect("png")).expect("write");
|
std::fs::write(
|
||||||
println!("{}: {} lines, complete={}", mode.name, image.lines, image.complete);
|
format!("{dir}/{name}-decoded.png"),
|
||||||
|
canvas.to_png().expect("png"),
|
||||||
|
)
|
||||||
|
.expect("write");
|
||||||
|
println!(
|
||||||
|
"{}: {} lines, complete={}",
|
||||||
|
mode.name, image.lines, image.complete
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ impl SstvDecoder {
|
|||||||
// named in the configuration, the first sync pulse is enough to start.
|
// named in the configuration, the first sync pulse is enough to start.
|
||||||
if let Some(mode) = self.config.force_mode.and_then(mode_for_vis) {
|
if let Some(mode) = self.config.force_mode.and_then(mode_for_vis) {
|
||||||
if let Some(sync) = self.find_sync(from, self.freqs.len(), mode) {
|
if let Some(sync) = self.find_sync(from, self.freqs.len(), mode) {
|
||||||
let line_start = (self.base + sync as u64) as f64 - ms_to_samples(mode.sync_offset_ms, self.sample_rate);
|
let line_start = (self.base + sync as u64) as f64
|
||||||
|
- ms_to_samples(mode.sync_offset_ms, self.sample_rate);
|
||||||
self.begin_at(mode, line_start.max(0.0), events);
|
self.begin_at(mode, line_start.max(0.0), events);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -384,7 +385,8 @@ impl SstvDecoder {
|
|||||||
if from >= self.freqs.len() {
|
if from >= self.freqs.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.find_sync(from, to, mode).map(|at| self.base + at as u64)
|
self.find_sync(from, to, mode)
|
||||||
|
.map(|at| self.base + at as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop what is behind the decoder, so a long reception does not grow the
|
/// Drop what is behind the decoder, so a long reception does not grow the
|
||||||
@@ -461,7 +463,10 @@ fn sample_scan(
|
|||||||
(from, to)
|
(from, to)
|
||||||
} else {
|
} else {
|
||||||
let at = (pixel_start - base as f64).max(0.0) as usize;
|
let at = (pixel_start - base as f64).max(0.0) as usize;
|
||||||
(at.min(freqs.len().saturating_sub(1)), (at + 1).min(freqs.len()))
|
(
|
||||||
|
at.min(freqs.len().saturating_sub(1)),
|
||||||
|
(at + 1).min(freqs.len()),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
if to <= from {
|
if to <= from {
|
||||||
out.push(0);
|
out.push(0);
|
||||||
@@ -503,14 +508,19 @@ fn compose_rows(reception: &mut Reception, scans: &[(Channel, Vec<u8>)]) -> Vec<
|
|||||||
// This line carries one chroma channel; the other is the one from
|
// This line carries one chroma channel; the other is the one from
|
||||||
// the line before, which is what the mode expects a decoder to do.
|
// the line before, which is what the mode expects a decoder to do.
|
||||||
let luma = find(Channel::LumaOdd).cloned().unwrap_or_default();
|
let luma = find(Channel::LumaOdd).cloned().unwrap_or_default();
|
||||||
let chroma = find(Channel::ChromaAlternating).cloned().unwrap_or_default();
|
let chroma = find(Channel::ChromaAlternating)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
if reception.row.is_multiple_of(2) {
|
if reception.row.is_multiple_of(2) {
|
||||||
reception.last_chroma_r = Some(chroma);
|
reception.last_chroma_r = Some(chroma);
|
||||||
} else {
|
} else {
|
||||||
reception.last_chroma_b = Some(chroma);
|
reception.last_chroma_b = Some(chroma);
|
||||||
}
|
}
|
||||||
let neutral = vec![128u8; luma.len().max(1) / 2];
|
let neutral = vec![128u8; luma.len().max(1) / 2];
|
||||||
let cr = reception.last_chroma_r.clone().unwrap_or_else(|| neutral.clone());
|
let cr = reception
|
||||||
|
.last_chroma_r
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| neutral.clone());
|
||||||
let cb = reception.last_chroma_b.clone().unwrap_or(neutral);
|
let cb = reception.last_chroma_b.clone().unwrap_or(neutral);
|
||||||
vec![ycrcb_row(&luma, &cr, &cb, width)]
|
vec![ycrcb_row(&luma, &cr, &cb, width)]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,13 @@ fn design_bandpass_fir(sample_rate: u32) -> Vec<f32> {
|
|||||||
let high = (f64::from(BAND_HIGH_HZ) / sr).min(0.499);
|
let high = (f64::from(BAND_HIGH_HZ) / sr).min(0.499);
|
||||||
let m = (BANDPASS_TAPS - 1) as f64;
|
let m = (BANDPASS_TAPS - 1) as f64;
|
||||||
let mid = m / 2.0;
|
let mid = m / 2.0;
|
||||||
let sinc = |x: f64| if x.abs() < 1e-9 { 1.0 } else { (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x) };
|
let sinc = |x: f64| {
|
||||||
|
if x.abs() < 1e-9 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
(std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x)
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut coeffs = Vec::with_capacity(BANDPASS_TAPS);
|
let mut coeffs = Vec::with_capacity(BANDPASS_TAPS);
|
||||||
for i in 0..BANDPASS_TAPS {
|
for i in 0..BANDPASS_TAPS {
|
||||||
let n = i as f64 - mid;
|
let n = i as f64 - mid;
|
||||||
@@ -223,7 +229,10 @@ mod tests {
|
|||||||
fn measure(freq: f32, sample_rate: u32) -> f32 {
|
fn measure(freq: f32, sample_rate: u32) -> f32 {
|
||||||
let mut demod = FreqDemod::new(sample_rate);
|
let mut demod = FreqDemod::new(sample_rate);
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
demod.process_into(&tone(freq, sample_rate, sample_rate as usize / 10), &mut out);
|
demod.process_into(
|
||||||
|
&tone(freq, sample_rate, sample_rate as usize / 10),
|
||||||
|
&mut out,
|
||||||
|
);
|
||||||
let settled = &out[HILBERT_TAPS * 2..];
|
let settled = &out[HILBERT_TAPS * 2..];
|
||||||
settled.iter().sum::<f32>() / settled.len() as f32
|
settled.iter().sum::<f32>() / settled.len() as f32
|
||||||
}
|
}
|
||||||
@@ -276,7 +285,13 @@ mod tests {
|
|||||||
// itself takes the filter's length to pass through.
|
// itself takes the filter's length to pass through.
|
||||||
let before = out[4800 - 200..4800 - 100].iter().sum::<f32>() / 100.0;
|
let before = out[4800 - 200..4800 - 100].iter().sum::<f32>() / 100.0;
|
||||||
let after = out[4800 + 200..4800 + 300].iter().sum::<f32>() / 100.0;
|
let after = out[4800 + 200..4800 + 300].iter().sum::<f32>() / 100.0;
|
||||||
assert!((before - 1500.0).abs() < 10.0, "before the step: {before:.1} Hz");
|
assert!(
|
||||||
assert!((after - 2300.0).abs() < 10.0, "after the step: {after:.1} Hz");
|
(before - 1500.0).abs() < 10.0,
|
||||||
|
"before the step: {before:.1} Hz"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(after - 2300.0).abs() < 10.0,
|
||||||
|
"after the step: {after:.1} Hz"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,22 @@ pub fn hz_from_level(level: u8) -> f32 {
|
|||||||
/// The tones of a VIS header announcing `vis`.
|
/// The tones of a VIS header announcing `vis`.
|
||||||
pub fn vis_tones(vis: u8) -> Vec<Tone> {
|
pub fn vis_tones(vis: u8) -> Vec<Tone> {
|
||||||
let mut tones = vec![
|
let mut tones = vec![
|
||||||
Tone { hz: 1900.0, ms: 300.0 },
|
Tone {
|
||||||
Tone { hz: 1200.0, ms: 10.0 },
|
hz: 1900.0,
|
||||||
Tone { hz: 1900.0, ms: 300.0 },
|
ms: 300.0,
|
||||||
Tone { hz: 1200.0, ms: 30.0 }, // start bit
|
},
|
||||||
|
Tone {
|
||||||
|
hz: 1200.0,
|
||||||
|
ms: 10.0,
|
||||||
|
},
|
||||||
|
Tone {
|
||||||
|
hz: 1900.0,
|
||||||
|
ms: 300.0,
|
||||||
|
},
|
||||||
|
Tone {
|
||||||
|
hz: 1200.0,
|
||||||
|
ms: 30.0,
|
||||||
|
}, // start bit
|
||||||
];
|
];
|
||||||
let mut ones = 0;
|
let mut ones = 0;
|
||||||
for bit in 0..7 {
|
for bit in 0..7 {
|
||||||
@@ -40,11 +52,20 @@ pub fn vis_tones(vis: u8) -> Vec<Tone> {
|
|||||||
if set {
|
if set {
|
||||||
ones += 1;
|
ones += 1;
|
||||||
}
|
}
|
||||||
tones.push(Tone { hz: if set { 1100.0 } else { 1300.0 }, ms: 30.0 });
|
tones.push(Tone {
|
||||||
|
hz: if set { 1100.0 } else { 1300.0 },
|
||||||
|
ms: 30.0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Even parity over the seven data bits.
|
// Even parity over the seven data bits.
|
||||||
tones.push(Tone { hz: if ones % 2 == 1 { 1100.0 } else { 1300.0 }, ms: 30.0 });
|
tones.push(Tone {
|
||||||
tones.push(Tone { hz: 1200.0, ms: 30.0 }); // stop bit
|
hz: if ones % 2 == 1 { 1100.0 } else { 1300.0 },
|
||||||
|
ms: 30.0,
|
||||||
|
});
|
||||||
|
tones.push(Tone {
|
||||||
|
hz: 1200.0,
|
||||||
|
ms: 30.0,
|
||||||
|
}); // stop bit
|
||||||
tones
|
tones
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +118,10 @@ pub fn encode_tones(mode: &SstvMode, frame: &Frame<'_>) -> Vec<Tone> {
|
|||||||
let pixel_ms = ms / pixels as f64;
|
let pixel_ms = ms / pixels as f64;
|
||||||
for x in 0..pixels {
|
for x in 0..pixels {
|
||||||
let level = channel_level(mode, frame, channel, x, top, transmission);
|
let level = channel_level(mode, frame, channel, x, top, transmission);
|
||||||
tones.push(Tone { hz: hz_from_level(level), ms: pixel_ms });
|
tones.push(Tone {
|
||||||
|
hz: hz_from_level(level),
|
||||||
|
ms: pixel_ms,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +173,8 @@ fn chroma(mode: &SstvMode, frame: &Frame<'_>, x: usize, top: usize, want_cr: boo
|
|||||||
/// demodulator sees no step at a tone boundary that isn't in the signal.
|
/// demodulator sees no step at a tone boundary that isn't in the signal.
|
||||||
pub fn render(tones: &[Tone], sample_rate: u32) -> Vec<f32> {
|
pub fn render(tones: &[Tone], sample_rate: u32) -> Vec<f32> {
|
||||||
let sr = f64::from(sample_rate);
|
let sr = f64::from(sample_rate);
|
||||||
let mut out = Vec::with_capacity((tones.iter().map(|t| t.ms).sum::<f64>() / 1000.0 * sr) as usize);
|
let mut out =
|
||||||
|
Vec::with_capacity((tones.iter().map(|t| t.ms).sum::<f64>() / 1000.0 * sr) as usize);
|
||||||
let mut phase = 0.0f64;
|
let mut phase = 0.0f64;
|
||||||
// Each tone's *end* is rounded to a sample, rather than its length: a
|
// Each tone's *end* is rounded to a sample, rather than its length: a
|
||||||
// pixel of 25.5 samples rounded up on its own puts a whole line 600
|
// pixel of 25.5 samples rounded up on its own puts a whole line 600
|
||||||
@@ -195,7 +220,11 @@ mod tests {
|
|||||||
fn a_transmission_lasts_as_long_as_the_mode_says() {
|
fn a_transmission_lasts_as_long_as_the_mode_says() {
|
||||||
let mode = mode_for_vis(44).expect("Martin M1");
|
let mode = mode_for_vis(44).expect("Martin M1");
|
||||||
let rgb = vec![128u8; 320 * 256 * 3];
|
let rgb = vec![128u8; 320 * 256 * 3];
|
||||||
let frame = Frame { width: 320, height: 256, rgb: &rgb };
|
let frame = Frame {
|
||||||
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
rgb: &rgb,
|
||||||
|
};
|
||||||
let audio = encode(mode, &frame, 48_000);
|
let audio = encode(mode, &frame, 48_000);
|
||||||
// Header plus 256 lines, within a line of the published duration.
|
// Header plus 256 lines, within a line of the published duration.
|
||||||
let header_s = 0.94;
|
let header_s = 0.94;
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ impl ImageCanvas {
|
|||||||
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
||||||
.collect();
|
.collect();
|
||||||
let path = dir.join(format!("SSTV_{stamp}_{freq_hz}_{slug}.png"));
|
let path = dir.join(format!("SSTV_{stamp}_{freq_hz}_{slug}.png"));
|
||||||
std::fs::write(&path, self.to_png()?).map_err(|e| format!("write {}: {e}", path.display()))?;
|
std::fs::write(&path, self.to_png()?)
|
||||||
|
.map_err(|e| format!("write {}: {e}", path.display()))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,22 +147,40 @@ impl SstvMode {
|
|||||||
const MARTIN_M1: &[Segment] = &[
|
const MARTIN_M1: &[Segment] = &[
|
||||||
Segment::Sync(4.862),
|
Segment::Sync(4.862),
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
Segment::Scan { channel: Channel::Green, ms: 146.432 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Green,
|
||||||
|
ms: 146.432,
|
||||||
|
},
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
Segment::Scan { channel: Channel::Blue, ms: 146.432 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Blue,
|
||||||
|
ms: 146.432,
|
||||||
|
},
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
Segment::Scan { channel: Channel::Red, ms: 146.432 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Red,
|
||||||
|
ms: 146.432,
|
||||||
|
},
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
];
|
];
|
||||||
|
|
||||||
const MARTIN_M2: &[Segment] = &[
|
const MARTIN_M2: &[Segment] = &[
|
||||||
Segment::Sync(4.862),
|
Segment::Sync(4.862),
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
Segment::Scan { channel: Channel::Green, ms: 73.216 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Green,
|
||||||
|
ms: 73.216,
|
||||||
|
},
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
Segment::Scan { channel: Channel::Blue, ms: 73.216 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Blue,
|
||||||
|
ms: 73.216,
|
||||||
|
},
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
Segment::Scan { channel: Channel::Red, ms: 73.216 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Red,
|
||||||
|
ms: 73.216,
|
||||||
|
},
|
||||||
Segment::Gap(0.572),
|
Segment::Gap(0.572),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -174,32 +192,59 @@ const MARTIN_M2: &[Segment] = &[
|
|||||||
|
|
||||||
const SCOTTIE_S1: &[Segment] = &[
|
const SCOTTIE_S1: &[Segment] = &[
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Green, ms: 138.240 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Green,
|
||||||
|
ms: 138.240,
|
||||||
|
},
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Blue, ms: 138.240 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Blue,
|
||||||
|
ms: 138.240,
|
||||||
|
},
|
||||||
Segment::Sync(9.0),
|
Segment::Sync(9.0),
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Red, ms: 138.240 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Red,
|
||||||
|
ms: 138.240,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SCOTTIE_S2: &[Segment] = &[
|
const SCOTTIE_S2: &[Segment] = &[
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Green, ms: 88.064 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Green,
|
||||||
|
ms: 88.064,
|
||||||
|
},
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Blue, ms: 88.064 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Blue,
|
||||||
|
ms: 88.064,
|
||||||
|
},
|
||||||
Segment::Sync(9.0),
|
Segment::Sync(9.0),
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Red, ms: 88.064 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Red,
|
||||||
|
ms: 88.064,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SCOTTIE_DX: &[Segment] = &[
|
const SCOTTIE_DX: &[Segment] = &[
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Green, ms: 345.6 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Green,
|
||||||
|
ms: 345.6,
|
||||||
|
},
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Blue, ms: 345.6 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Blue,
|
||||||
|
ms: 345.6,
|
||||||
|
},
|
||||||
Segment::Sync(9.0),
|
Segment::Sync(9.0),
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::Red, ms: 345.6 },
|
Segment::Scan {
|
||||||
|
channel: Channel::Red,
|
||||||
|
ms: 345.6,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Scottie's sync arrives after the green and blue scans: 1.5 + 138.24 + 1.5 +
|
/// Scottie's sync arrives after the green and blue scans: 1.5 + 138.24 + 1.5 +
|
||||||
@@ -215,22 +260,37 @@ const fn scottie_sync_offset(scan_ms: f64) -> f64 {
|
|||||||
const ROBOT_36: &[Segment] = &[
|
const ROBOT_36: &[Segment] = &[
|
||||||
Segment::Sync(9.0),
|
Segment::Sync(9.0),
|
||||||
Segment::Gap(3.0),
|
Segment::Gap(3.0),
|
||||||
Segment::Scan { channel: Channel::LumaOdd, ms: 88.0 },
|
Segment::Scan {
|
||||||
|
channel: Channel::LumaOdd,
|
||||||
|
ms: 88.0,
|
||||||
|
},
|
||||||
Segment::Gap(4.5),
|
Segment::Gap(4.5),
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::ChromaAlternating, ms: 44.0 },
|
Segment::Scan {
|
||||||
|
channel: Channel::ChromaAlternating,
|
||||||
|
ms: 44.0,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROBOT_72: &[Segment] = &[
|
const ROBOT_72: &[Segment] = &[
|
||||||
Segment::Sync(9.0),
|
Segment::Sync(9.0),
|
||||||
Segment::Gap(3.0),
|
Segment::Gap(3.0),
|
||||||
Segment::Scan { channel: Channel::LumaOdd, ms: 138.0 },
|
Segment::Scan {
|
||||||
|
channel: Channel::LumaOdd,
|
||||||
|
ms: 138.0,
|
||||||
|
},
|
||||||
Segment::Gap(4.5),
|
Segment::Gap(4.5),
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::ChromaR, ms: 69.0 },
|
Segment::Scan {
|
||||||
|
channel: Channel::ChromaR,
|
||||||
|
ms: 69.0,
|
||||||
|
},
|
||||||
Segment::Gap(4.5),
|
Segment::Gap(4.5),
|
||||||
Segment::Gap(1.5),
|
Segment::Gap(1.5),
|
||||||
Segment::Scan { channel: Channel::ChromaB, ms: 69.0 },
|
Segment::Scan {
|
||||||
|
channel: Channel::ChromaB,
|
||||||
|
ms: 69.0,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -243,10 +303,22 @@ macro_rules! pd_segments {
|
|||||||
const $name: &[Segment] = &[
|
const $name: &[Segment] = &[
|
||||||
Segment::Sync(20.0),
|
Segment::Sync(20.0),
|
||||||
Segment::Gap(2.08),
|
Segment::Gap(2.08),
|
||||||
Segment::Scan { channel: Channel::LumaOdd, ms: $scan },
|
Segment::Scan {
|
||||||
Segment::Scan { channel: Channel::ChromaR, ms: $scan },
|
channel: Channel::LumaOdd,
|
||||||
Segment::Scan { channel: Channel::ChromaB, ms: $scan },
|
ms: $scan,
|
||||||
Segment::Scan { channel: Channel::LumaEven, ms: $scan },
|
},
|
||||||
|
Segment::Scan {
|
||||||
|
channel: Channel::ChromaR,
|
||||||
|
ms: $scan,
|
||||||
|
},
|
||||||
|
Segment::Scan {
|
||||||
|
channel: Channel::ChromaB,
|
||||||
|
ms: $scan,
|
||||||
|
},
|
||||||
|
Segment::Scan {
|
||||||
|
channel: Channel::LumaEven,
|
||||||
|
ms: $scan,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -266,87 +338,186 @@ pd_segments!(PD_290, 228.800);
|
|||||||
const WRAASE_SC2_180: &[Segment] = &[
|
const WRAASE_SC2_180: &[Segment] = &[
|
||||||
Segment::Sync(5.5225),
|
Segment::Sync(5.5225),
|
||||||
Segment::Gap(0.5),
|
Segment::Gap(0.5),
|
||||||
Segment::Scan { channel: Channel::Red, ms: 235.0 },
|
Segment::Scan {
|
||||||
Segment::Scan { channel: Channel::Green, ms: 235.0 },
|
channel: Channel::Red,
|
||||||
Segment::Scan { channel: Channel::Blue, ms: 235.0 },
|
ms: 235.0,
|
||||||
|
},
|
||||||
|
Segment::Scan {
|
||||||
|
channel: Channel::Green,
|
||||||
|
ms: 235.0,
|
||||||
|
},
|
||||||
|
Segment::Scan {
|
||||||
|
channel: Channel::Blue,
|
||||||
|
ms: 235.0,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Every mode this decoder knows, in VIS order.
|
/// Every mode this decoder knows, in VIS order.
|
||||||
pub static MODES: &[SstvMode] = &[
|
pub static MODES: &[SstvMode] = &[
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 8, name: "Robot 36", width: 320, height: 240, line_ms: 150.0,
|
vis: 8,
|
||||||
segments: ROBOT_36, color: ColorModel::YCrCbAlternating,
|
name: "Robot 36",
|
||||||
lines_per_transmission: 1, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
line_ms: 150.0,
|
||||||
|
segments: ROBOT_36,
|
||||||
|
color: ColorModel::YCrCbAlternating,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 12, name: "Robot 72", width: 320, height: 240, line_ms: 300.0,
|
vis: 12,
|
||||||
segments: ROBOT_72, color: ColorModel::YCrCb,
|
name: "Robot 72",
|
||||||
lines_per_transmission: 1, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
line_ms: 300.0,
|
||||||
|
segments: ROBOT_72,
|
||||||
|
color: ColorModel::YCrCb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 40, name: "Martin M2", width: 320, height: 256, line_ms: 226.798,
|
vis: 40,
|
||||||
segments: MARTIN_M2, color: ColorModel::Rgb,
|
name: "Martin M2",
|
||||||
lines_per_transmission: 1, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 226.798,
|
||||||
|
segments: MARTIN_M2,
|
||||||
|
color: ColorModel::Rgb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 44, name: "Martin M1", width: 320, height: 256, line_ms: 446.446,
|
vis: 44,
|
||||||
segments: MARTIN_M1, color: ColorModel::Rgb,
|
name: "Martin M1",
|
||||||
lines_per_transmission: 1, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 446.446,
|
||||||
|
segments: MARTIN_M1,
|
||||||
|
color: ColorModel::Rgb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 55, name: "Wraase SC2-180", width: 320, height: 256, line_ms: 711.0225,
|
vis: 55,
|
||||||
segments: WRAASE_SC2_180, color: ColorModel::Rgb,
|
name: "Wraase SC2-180",
|
||||||
lines_per_transmission: 1, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 711.0225,
|
||||||
|
segments: WRAASE_SC2_180,
|
||||||
|
color: ColorModel::Rgb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 56, name: "Scottie S2", width: 320, height: 256, line_ms: 277.692,
|
vis: 56,
|
||||||
segments: SCOTTIE_S2, color: ColorModel::Rgb,
|
name: "Scottie S2",
|
||||||
lines_per_transmission: 1, sync_offset_ms: scottie_sync_offset(88.064),
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 277.692,
|
||||||
|
segments: SCOTTIE_S2,
|
||||||
|
color: ColorModel::Rgb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: scottie_sync_offset(88.064),
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 60, name: "Scottie S1", width: 320, height: 256, line_ms: 428.22,
|
vis: 60,
|
||||||
segments: SCOTTIE_S1, color: ColorModel::Rgb,
|
name: "Scottie S1",
|
||||||
lines_per_transmission: 1, sync_offset_ms: scottie_sync_offset(138.240),
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 428.22,
|
||||||
|
segments: SCOTTIE_S1,
|
||||||
|
color: ColorModel::Rgb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: scottie_sync_offset(138.240),
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 76, name: "Scottie DX", width: 320, height: 256, line_ms: 1050.3,
|
vis: 76,
|
||||||
segments: SCOTTIE_DX, color: ColorModel::Rgb,
|
name: "Scottie DX",
|
||||||
lines_per_transmission: 1, sync_offset_ms: scottie_sync_offset(345.6),
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 1050.3,
|
||||||
|
segments: SCOTTIE_DX,
|
||||||
|
color: ColorModel::Rgb,
|
||||||
|
lines_per_transmission: 1,
|
||||||
|
sync_offset_ms: scottie_sync_offset(345.6),
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 93, name: "PD50", width: 320, height: 256, line_ms: 388.16,
|
vis: 93,
|
||||||
segments: PD_50, color: ColorModel::YCrCbPaired,
|
name: "PD50",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 388.16,
|
||||||
|
segments: PD_50,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 94, name: "PD290", width: 800, height: 616, line_ms: 937.28,
|
vis: 94,
|
||||||
segments: PD_290, color: ColorModel::YCrCbPaired,
|
name: "PD290",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 800,
|
||||||
|
height: 616,
|
||||||
|
line_ms: 937.28,
|
||||||
|
segments: PD_290,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 95, name: "PD120", width: 640, height: 496, line_ms: 508.48,
|
vis: 95,
|
||||||
segments: PD_120, color: ColorModel::YCrCbPaired,
|
name: "PD120",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 640,
|
||||||
|
height: 496,
|
||||||
|
line_ms: 508.48,
|
||||||
|
segments: PD_120,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 96, name: "PD180", width: 640, height: 496, line_ms: 754.24,
|
vis: 96,
|
||||||
segments: PD_180, color: ColorModel::YCrCbPaired,
|
name: "PD180",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 640,
|
||||||
|
height: 496,
|
||||||
|
line_ms: 754.24,
|
||||||
|
segments: PD_180,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 97, name: "PD240", width: 640, height: 496, line_ms: 1000.768,
|
vis: 97,
|
||||||
segments: PD_240, color: ColorModel::YCrCbPaired,
|
name: "PD240",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 640,
|
||||||
|
height: 496,
|
||||||
|
line_ms: 1000.768,
|
||||||
|
segments: PD_240,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 98, name: "PD160", width: 512, height: 400, line_ms: 804.416,
|
vis: 98,
|
||||||
segments: PD_160, color: ColorModel::YCrCbPaired,
|
name: "PD160",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 512,
|
||||||
|
height: 400,
|
||||||
|
line_ms: 804.416,
|
||||||
|
segments: PD_160,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
SstvMode {
|
SstvMode {
|
||||||
vis: 99, name: "PD90", width: 320, height: 256, line_ms: 703.04,
|
vis: 99,
|
||||||
segments: PD_90, color: ColorModel::YCrCbPaired,
|
name: "PD90",
|
||||||
lines_per_transmission: 2, sync_offset_ms: 0.0,
|
width: 320,
|
||||||
|
height: 256,
|
||||||
|
line_ms: 703.04,
|
||||||
|
segments: PD_90,
|
||||||
|
color: ColorModel::YCrCbPaired,
|
||||||
|
lines_per_transmission: 2,
|
||||||
|
sync_offset_ms: 0.0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -377,7 +548,9 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
(sum - mode.line_ms).abs() < 0.001,
|
(sum - mode.line_ms).abs() < 0.001,
|
||||||
"{}: segments total {:.4} ms, line time says {:.4} ms",
|
"{}: segments total {:.4} ms, line time says {:.4} ms",
|
||||||
mode.name, sum, mode.line_ms,
|
mode.name,
|
||||||
|
sum,
|
||||||
|
mode.line_ms,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,7 +577,13 @@ mod tests {
|
|||||||
ColorModel::YCrCb => 3,
|
ColorModel::YCrCb => 3,
|
||||||
ColorModel::YCrCbPaired => 4,
|
ColorModel::YCrCbPaired => 4,
|
||||||
};
|
};
|
||||||
assert_eq!(scans.len(), expected, "{} has {} scans", mode.name, scans.len());
|
assert_eq!(
|
||||||
|
scans.len(),
|
||||||
|
expected,
|
||||||
|
"{} has {} scans",
|
||||||
|
mode.name,
|
||||||
|
scans.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,7 +596,10 @@ mod tests {
|
|||||||
let actual = mode.frame_secs();
|
let actual = mode.frame_secs();
|
||||||
assert!(
|
assert!(
|
||||||
(actual - secs).abs() < 1.5,
|
(actual - secs).abs() < 1.5,
|
||||||
"{} takes {:.1} s, expected about {:.0} s", mode.name, actual, secs,
|
"{} takes {:.1} s, expected about {:.0} s",
|
||||||
|
mode.name,
|
||||||
|
actual,
|
||||||
|
secs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,11 @@ mod tests {
|
|||||||
let freqs = freqs_from_tones(&vis_tones(mode.vis), 48_000);
|
let freqs = freqs_from_tones(&vis_tones(mode.vis), 48_000);
|
||||||
let hit = find_vis(&freqs, 48_000, 0)
|
let hit = find_vis(&freqs, 48_000, 0)
|
||||||
.unwrap_or_else(|| panic!("{} header not found", mode.name));
|
.unwrap_or_else(|| panic!("{} header not found", mode.name));
|
||||||
assert_eq!(hit.code, mode.vis, "{} decoded as VIS {}", mode.name, hit.code);
|
assert_eq!(
|
||||||
|
hit.code, mode.vis,
|
||||||
|
"{} decoded as VIS {}",
|
||||||
|
mode.name, hit.code
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +192,8 @@ mod tests {
|
|||||||
let slack = sample_rate as usize / 100; // 10 ms
|
let slack = sample_rate as usize / 100; // 10 ms
|
||||||
assert!(
|
assert!(
|
||||||
hit.image_start.abs_diff(expected) < slack,
|
hit.image_start.abs_diff(expected) < slack,
|
||||||
"image starts at {}, expected about {expected}", hit.image_start,
|
"image starts at {}, expected about {expected}",
|
||||||
|
hit.image_start,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +204,11 @@ mod tests {
|
|||||||
// Flip the parity bit alone: seven data bits still say Martin M1, but
|
// Flip the parity bit alone: seven data bits still say Martin M1, but
|
||||||
// nothing now vouches for them.
|
// nothing now vouches for them.
|
||||||
let parity = tones.len() - 2;
|
let parity = tones.len() - 2;
|
||||||
tones[parity].hz = if tones[parity].hz == ONE_HZ { ZERO_HZ } else { ONE_HZ };
|
tones[parity].hz = if tones[parity].hz == ONE_HZ {
|
||||||
|
ZERO_HZ
|
||||||
|
} else {
|
||||||
|
ONE_HZ
|
||||||
|
};
|
||||||
let freqs = freqs_from_tones(&tones, sample_rate);
|
let freqs = freqs_from_tones(&tones, sample_rate);
|
||||||
assert_eq!(find_vis(&freqs, sample_rate, 0), None);
|
assert_eq!(find_vis(&freqs, sample_rate, 0), None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,16 +134,38 @@ fn round_trip(vis: u8, tolerance: f64) {
|
|||||||
let width = usize::from(mode.width);
|
let width = usize::from(mode.width);
|
||||||
let height = usize::from(mode.height);
|
let height = usize::from(mode.height);
|
||||||
let sent = test_card(width, height);
|
let sent = test_card(width, height);
|
||||||
let frame = Frame { width, height, rgb: &sent };
|
let frame = Frame {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb: &sent,
|
||||||
|
};
|
||||||
let audio = encode(mode, &frame, SAMPLE_RATE);
|
let audio = encode(mode, &frame, SAMPLE_RATE);
|
||||||
|
|
||||||
let (images, rows) = decode(&audio);
|
let (images, rows) = decode(&audio);
|
||||||
assert_eq!(images.len(), 1, "{}: expected one picture, got {}", mode.name, images.len());
|
assert_eq!(
|
||||||
|
images.len(),
|
||||||
|
1,
|
||||||
|
"{}: expected one picture, got {}",
|
||||||
|
mode.name,
|
||||||
|
images.len()
|
||||||
|
);
|
||||||
let image = &images[0];
|
let image = &images[0];
|
||||||
assert!(image.complete, "{}: reception did not reach the bottom", mode.name);
|
assert!(
|
||||||
|
image.complete,
|
||||||
|
"{}: reception did not reach the bottom",
|
||||||
|
mode.name
|
||||||
|
);
|
||||||
assert_eq!(image.mode, mode.name);
|
assert_eq!(image.mode, mode.name);
|
||||||
assert_eq!(image.lines, mode.height, "{}: {} of {} lines", mode.name, image.lines, mode.height);
|
assert_eq!(
|
||||||
assert_eq!(rows, height, "{}: emitted {rows} rows for {height} lines", mode.name);
|
image.lines, mode.height,
|
||||||
|
"{}: {} of {} lines",
|
||||||
|
mode.name, image.lines, mode.height
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rows, height,
|
||||||
|
"{}: emitted {rows} rows for {height} lines",
|
||||||
|
mode.name
|
||||||
|
);
|
||||||
|
|
||||||
let error = interior_error(width, height, &sent, &image.rgb);
|
let error = interior_error(width, height, &sent, &image.rgb);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -209,13 +231,21 @@ fn survives_silence_around_the_transmission() {
|
|||||||
let mode = mode_for_vis(44).expect("Martin M1");
|
let mode = mode_for_vis(44).expect("Martin M1");
|
||||||
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
||||||
let sent = test_card(width, height);
|
let sent = test_card(width, height);
|
||||||
let frame = Frame { width, height, rgb: &sent };
|
let frame = Frame {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb: &sent,
|
||||||
|
};
|
||||||
let mut audio = vec![0.0f32; SAMPLE_RATE as usize * 2];
|
let mut audio = vec![0.0f32; SAMPLE_RATE as usize * 2];
|
||||||
audio.extend(encode(mode, &frame, SAMPLE_RATE));
|
audio.extend(encode(mode, &frame, SAMPLE_RATE));
|
||||||
audio.extend(std::iter::repeat_n(0.0, SAMPLE_RATE as usize));
|
audio.extend(std::iter::repeat_n(0.0, SAMPLE_RATE as usize));
|
||||||
|
|
||||||
let (images, _) = decode(&audio);
|
let (images, _) = decode(&audio);
|
||||||
assert_eq!(images.len(), 1, "expected one picture from a transmission in silence");
|
assert_eq!(
|
||||||
|
images.len(),
|
||||||
|
1,
|
||||||
|
"expected one picture from a transmission in silence"
|
||||||
|
);
|
||||||
assert!(images[0].complete);
|
assert!(images[0].complete);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +256,11 @@ fn a_truncated_transmission_still_yields_its_lines() {
|
|||||||
let mode = mode_for_vis(44).expect("Martin M1");
|
let mode = mode_for_vis(44).expect("Martin M1");
|
||||||
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
||||||
let sent = test_card(width, height);
|
let sent = test_card(width, height);
|
||||||
let frame = Frame { width, height, rgb: &sent };
|
let frame = Frame {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb: &sent,
|
||||||
|
};
|
||||||
let full = encode(mode, &frame, SAMPLE_RATE);
|
let full = encode(mode, &frame, SAMPLE_RATE);
|
||||||
// Two thirds of the picture, then silence for long enough that the decoder
|
// Two thirds of the picture, then silence for long enough that the decoder
|
||||||
// stops waiting for the rest.
|
// stops waiting for the rest.
|
||||||
@@ -234,17 +268,29 @@ fn a_truncated_transmission_still_yields_its_lines() {
|
|||||||
audio.extend(std::iter::repeat_n(0.0, SAMPLE_RATE as usize * 5));
|
audio.extend(std::iter::repeat_n(0.0, SAMPLE_RATE as usize * 5));
|
||||||
|
|
||||||
let (images, _) = decode(&audio);
|
let (images, _) = decode(&audio);
|
||||||
assert_eq!(images.len(), 1, "a cut-off transmission produced no picture");
|
assert_eq!(
|
||||||
|
images.len(),
|
||||||
|
1,
|
||||||
|
"a cut-off transmission produced no picture"
|
||||||
|
);
|
||||||
let image = &images[0];
|
let image = &images[0];
|
||||||
assert!(!image.complete, "a two-thirds transmission reported as complete");
|
assert!(
|
||||||
|
!image.complete,
|
||||||
|
"a two-thirds transmission reported as complete"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
image.lines > mode.height / 2 && image.lines < mode.height,
|
image.lines > mode.height / 2 && image.lines < mode.height,
|
||||||
"{} lines of {} arrived", image.lines, mode.height,
|
"{} lines of {} arrived",
|
||||||
|
image.lines,
|
||||||
|
mode.height,
|
||||||
);
|
);
|
||||||
// What did arrive is the top of the picture, and it is right.
|
// What did arrive is the top of the picture, and it is right.
|
||||||
let rows = usize::from(image.lines).saturating_sub(4);
|
let rows = usize::from(image.lines).saturating_sub(4);
|
||||||
let error = mean_error(&sent[..width * rows * 3], &image.rgb[..width * rows * 3]);
|
let error = mean_error(&sent[..width * rows * 3], &image.rgb[..width * rows * 3]);
|
||||||
assert!(error < 12.0, "the lines that arrived are wrong: mean error {error:.1}");
|
assert!(
|
||||||
|
error < 12.0,
|
||||||
|
"the lines that arrived are wrong: mean error {error:.1}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two pictures back to back: the decoder has to finish the first and pick up
|
/// Two pictures back to back: the decoder has to finish the first and pick up
|
||||||
@@ -254,7 +300,11 @@ fn decodes_a_second_transmission_after_the_first() {
|
|||||||
let mode = mode_for_vis(40).expect("Martin M2");
|
let mode = mode_for_vis(40).expect("Martin M2");
|
||||||
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
||||||
let sent = test_card(width, height);
|
let sent = test_card(width, height);
|
||||||
let frame = Frame { width, height, rgb: &sent };
|
let frame = Frame {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb: &sent,
|
||||||
|
};
|
||||||
let one = encode(mode, &frame, SAMPLE_RATE);
|
let one = encode(mode, &frame, SAMPLE_RATE);
|
||||||
|
|
||||||
let mut audio = one.clone();
|
let mut audio = one.clone();
|
||||||
@@ -262,8 +312,16 @@ fn decodes_a_second_transmission_after_the_first() {
|
|||||||
audio.extend(one);
|
audio.extend(one);
|
||||||
|
|
||||||
let (images, _) = decode(&audio);
|
let (images, _) = decode(&audio);
|
||||||
assert_eq!(images.len(), 2, "expected two pictures, got {}", images.len());
|
assert_eq!(
|
||||||
assert!(images.iter().all(|image| image.complete), "a picture did not finish");
|
images.len(),
|
||||||
|
2,
|
||||||
|
"expected two pictures, got {}",
|
||||||
|
images.len()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
images.iter().all(|image| image.complete),
|
||||||
|
"a picture did not finish"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Noise on the signal is the normal condition on HF. The picture should
|
/// Noise on the signal is the normal condition on HF. The picture should
|
||||||
@@ -273,7 +331,11 @@ fn decodes_through_noise() {
|
|||||||
let mode = mode_for_vis(44).expect("Martin M1");
|
let mode = mode_for_vis(44).expect("Martin M1");
|
||||||
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
||||||
let sent = test_card(width, height);
|
let sent = test_card(width, height);
|
||||||
let frame = Frame { width, height, rgb: &sent };
|
let frame = Frame {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb: &sent,
|
||||||
|
};
|
||||||
let clean = encode(mode, &frame, SAMPLE_RATE);
|
let clean = encode(mode, &frame, SAMPLE_RATE);
|
||||||
|
|
||||||
// Deterministic pseudo-noise at about 20 dB below the signal.
|
// Deterministic pseudo-noise at about 20 dB below the signal.
|
||||||
@@ -303,7 +365,11 @@ fn tolerates_a_transmitter_clock_that_runs_fast() {
|
|||||||
let mode = mode_for_vis(44).expect("Martin M1");
|
let mode = mode_for_vis(44).expect("Martin M1");
|
||||||
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
let (width, height) = (usize::from(mode.width), usize::from(mode.height));
|
||||||
let sent = test_card(width, height);
|
let sent = test_card(width, height);
|
||||||
let frame = Frame { width, height, rgb: &sent };
|
let frame = Frame {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb: &sent,
|
||||||
|
};
|
||||||
// Encoding at a slightly different rate and decoding at 48 kHz is exactly
|
// Encoding at a slightly different rate and decoding at 48 kHz is exactly
|
||||||
// a clock error: every duration is stretched by the same factor.
|
// a clock error: every duration is stretched by the same factor.
|
||||||
let audio = encode(mode, &frame, 48_048);
|
let audio = encode(mode, &frame, 48_048);
|
||||||
@@ -311,5 +377,8 @@ fn tolerates_a_transmitter_clock_that_runs_fast() {
|
|||||||
let (images, _) = decode(&audio);
|
let (images, _) = decode(&audio);
|
||||||
assert_eq!(images.len(), 1, "a 0.1% clock error cost the picture");
|
assert_eq!(images.len(), 1, "a 0.1% clock error cost the picture");
|
||||||
let error = interior_error(width, height, &sent, &images[0].rgb);
|
let error = interior_error(width, height, &sent, &images[0].rgb);
|
||||||
assert!(error < 12.0, "mean error with a fast clock {error:.1} levels");
|
assert!(
|
||||||
|
error < 12.0,
|
||||||
|
"mean error with a fast clock {error:.1} levels"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user