#![cfg(all(feature = "mod", feature = "demod"))]
use yodel::demodulator::{AfskDemodulator, DemodulatorConfig};
use yodel::modulator::{Modulator, ModulatorConfig};
use yodel::{Bit, ModemProfile, SampleRate};
const SR_HZ: u32 = 48_000;
struct Lcg(u64);
impl Lcg {
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 52) as f64 - 1.0
}
fn next_bit(&mut self) -> Bit {
if self.next_u64() >> 63 == 1 {
Bit::One
} else {
Bit::Zero
}
}
}
fn noise_peak(snr_db: f64) -> f64 {
let signal_rms = 32_767.0 / core::f64::consts::SQRT_2;
let noise_rms = signal_rms / 10f64.powf(snr_db / 20.0);
noise_rms * 3f64.sqrt()
}
fn mix(sample: i16, rng: &mut Lcg, peak: f64) -> i16 {
(f64::from(sample) + rng.next_f64() * peak).clamp(f64::from(i16::MIN), f64::from(i16::MAX))
as i16
}
fn eb_n0_db(snr_db: f64, baud_bps: u32) -> f64 {
snr_db + 10.0 * (f64::from(SR_HZ / 2) / f64::from(baud_bps)).log10()
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Bell202,
Hf300,
#[cfg(feature = "g3ruh")]
G3ruh9600,
}
const MODES: &[Mode] = &[
Mode::Bell202,
Mode::Hf300,
#[cfg(feature = "g3ruh")]
Mode::G3ruh9600,
];
const MIN_MODES: usize = 2;
impl Mode {
fn label(self) -> &'static str {
match self {
Mode::Bell202 => "Bell 202 1200 Bd",
Mode::Hf300 => "HF APRS 300 Bd",
#[cfg(feature = "g3ruh")]
Mode::G3ruh9600 => "G3RUH 9600 Bd",
}
}
fn profile(self) -> ModemProfile {
match self {
Mode::Bell202 => ModemProfile::BELL_202,
Mode::Hf300 => ModemProfile::HF_APRS_300,
#[cfg(feature = "g3ruh")]
Mode::G3ruh9600 => ModemProfile::G3RUH_9600,
}
}
fn baud_bps(self) -> u32 {
self.profile().baud().bps()
}
fn channel(self, bits: &[Bit], snr_db: f64, seed: u64) -> Vec<Bit> {
#[cfg(feature = "g3ruh")]
if self == Mode::G3ruh9600 {
return self.baseband_channel(bits, snr_db, seed);
}
self.tone_channel(bits, snr_db, seed)
}
fn tone_channel(self, bits: &[Bit], snr_db: f64, seed: u64) -> Vec<Bit> {
let sr = SampleRate::new(SR_HZ).unwrap();
let profile = self.profile();
let mcfg = ModulatorConfig::new(sr, profile.baud(), profile.tones()).unwrap();
let dcfg = DemodulatorConfig::new(sr, profile.baud(), profile.tones()).unwrap();
let mut demod = AfskDemodulator::new(dcfg).unwrap();
let mut rng = Lcg(seed);
let peak = noise_peak(snr_db);
let mut out = Vec::with_capacity(bits.len() + 16);
for s in Modulator::new(mcfg).i16_samples(bits.iter().copied()) {
if let Some(b) = demod.push_sample_i16(mix(s, &mut rng, peak)) {
out.push(b);
}
}
out
}
#[cfg(feature = "g3ruh")]
fn baseband_channel(self, bits: &[Bit], snr_db: f64, seed: u64) -> Vec<Bit> {
use yodel::{BasebandDemodulator, BasebandModulator};
let sr = SampleRate::new(SR_HZ).unwrap();
let baud = self.profile().baud();
let mut demod = BasebandDemodulator::new(sr, baud).unwrap();
let mut rng = Lcg(seed);
let peak = noise_peak(snr_db);
let mut out = Vec::with_capacity(bits.len() + 16);
for s in BasebandModulator::new(sr, baud)
.unwrap()
.i16_samples(bits.iter().copied())
{
if let Some(b) = demod.push_i16(mix(s, &mut rng, peak)) {
out.push(b);
}
}
out
}
}
const PREAMBLE_BITS: usize = 64;
const PAYLOAD_BITS: usize = 20_000;
const BURST_PAYLOAD_BITS: usize = 512;
const BURSTS: usize = 40;
const GUARD_BITS: usize = 24;
const MAX_ALIGN: usize = 12;
const CLEAN_SNR_DB: f64 = 20.0;
struct Aligned {
offset: usize,
errors: usize,
compared: usize,
runner_up_errors: usize,
}
fn tx_sequence(seed: u64, payload_bits: usize) -> Vec<Bit> {
let mut rng = Lcg(seed);
let mut v: Vec<Bit> = (0..PREAMBLE_BITS)
.map(|i| {
if i.is_multiple_of(2) {
Bit::One
} else {
Bit::Zero
}
})
.collect();
v.extend((0..payload_bits).map(|_| rng.next_bit()));
v
}
fn count_at(tx: &[Bit], rx: &[Bit], offset: usize, start: usize, end: usize) -> usize {
(start..end).filter(|&i| rx[i + offset] != tx[i]).count()
}
fn score(tx: &[Bit], rx: &[Bit], guard: usize) -> Aligned {
let start = PREAMBLE_BITS + guard;
let end = (tx.len() - guard).min(rx.len().saturating_sub(MAX_ALIGN));
assert!(
end > start + (tx.len() - PREAMBLE_BITS) / 2,
"demodulator produced only {} bits for {} transmitted: nothing to score",
rx.len(),
tx.len()
);
let mut best = (usize::MAX, 0usize);
let mut runner_up = usize::MAX;
for off in 0..=MAX_ALIGN {
let errors = count_at(tx, rx, off, start, end);
if errors < best.0 {
runner_up = best.0;
best = (errors, off);
} else if errors < runner_up {
runner_up = errors;
}
}
Aligned {
offset: best.1,
errors: best.0,
compared: end - start,
runner_up_errors: runner_up,
}
}
fn clean_alignment(mode: Mode) -> Aligned {
let tx = tx_sequence(0xA119_0001, PAYLOAD_BITS);
let rx = mode.channel(&tx, CLEAN_SNR_DB, 0xA119_1000);
score(&tx, &rx, GUARD_BITS * 2)
}
fn continuous_ber(mode: Mode, snr_db: f64, seed: u64) -> Aligned {
let tx = tx_sequence(seed ^ 0x5EED_B173, PAYLOAD_BITS);
let rx = mode.channel(&tx, snr_db, seed);
score(&tx, &rx, GUARD_BITS * 2)
}
fn burst_ber(mode: Mode, snr_db: f64, seed: u64) -> (usize, usize, usize) {
let mut errors = 0usize;
let mut compared = 0usize;
let mut max_lag = 0usize;
for b in 0..BURSTS {
let step = (b as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
let tx = tx_sequence(seed ^ 0x8082_5741 ^ step, BURST_PAYLOAD_BITS);
let rx = mode.channel(&tx, snr_db, seed.wrapping_add(step));
let a = score(&tx, &rx, GUARD_BITS);
errors += a.errors;
compared += a.compared;
max_lag = max_lag.max(a.offset);
}
(errors, compared, max_lag)
}
fn perfect_clock_ber(mode: Mode, snr_db: f64, seed: u64) -> Option<f64> {
#[cfg(feature = "g3ruh")]
if mode == Mode::G3ruh9600 {
return None;
}
use yodel::{Discriminator, QuadratureCorrelator};
let sr = SampleRate::new(SR_HZ).unwrap();
let profile = mode.profile();
let baud = profile.baud();
assert_eq!(
SR_HZ % baud.bps(),
0,
"ideal-centre indexing assumes a whole number of samples per bit"
);
let spb = (SR_HZ / baud.bps()) as usize;
let bits = tx_sequence(seed ^ 0x1DEA_1C10, PAYLOAD_BITS);
let start = PREAMBLE_BITS + GUARD_BITS * 2;
let end = bits.len() - GUARD_BITS * 2;
let metrics = |peak: f64| -> Vec<i32> {
let mut rng = Lcg(seed);
let mcfg = ModulatorConfig::new(sr, baud, profile.tones()).unwrap();
let mut corr = QuadratureCorrelator::new(sr, baud, profile.tones()).unwrap();
Modulator::new(mcfg)
.i16_samples(bits.iter().copied())
.map(|s| corr.push_i16(mix(s, &mut rng, peak)))
.collect()
};
let clean = metrics(0.0);
let end = end.min(clean.len() / spb - 2);
assert!(
end > start + PAYLOAD_BITS / 2,
"{}: only {} bit centres available to score of {PAYLOAD_BITS} \
transmitted — nothing to conclude from",
mode.label(),
end.saturating_sub(start)
);
let margin = |src: &[i32], d: usize| -> i64 {
(start..end)
.map(|k| {
let m = i64::from(src[k * spb + d]);
if bits[k] == Bit::One { m } else { -m }
})
.sum()
};
let delay = (0..2 * spb)
.max_by_key(|&d| margin(&clean, d))
.expect("at least one sample per bit");
assert!(
margin(&clean, delay) > 0,
"{}: no sampling delay in two bit periods gives a positive clean-signal \
correlation margin — the reference tap is broken, not merely mistimed",
mode.label()
);
let noisy = metrics(noise_peak(snr_db));
let errors = (start..end)
.filter(|&k| {
let decided = if noisy[k * spb + delay] > 0 {
Bit::One
} else {
Bit::Zero
};
decided != bits[k]
})
.count();
Some(errors as f64 / (end - start) as f64)
}
struct Rung {
snr_db: f64,
continuous: Option<f64>,
burst: Option<f64>,
perfect_clock: Option<f64>,
}
const MONOTONE_SLACK: f64 = 4e-4;
const MONOTONE_REL_SLACK: f64 = 0.08;
const MIN_RUNGS: usize = 6;
const MIN_PINNED_CEILINGS: usize = 8;
const SEEDS_PER_RUNG: usize = 4;
fn check_ladder(mode: Mode, rungs: &[Rung], seed: u64) {
assert!(
rungs.len() >= MIN_RUNGS,
"{}: ladder carries {} rungs, floor is {MIN_RUNGS}",
mode.label(),
rungs.len()
);
let baud = mode.baud_bps();
println!(
"\n{} @ {SR_HZ} Hz — raw BER at the demodulator output\n\
{PAYLOAD_BITS} bits continuous, {BURSTS}x{BURST_PAYLOAD_BITS} burst; \
`lag` is the winning alignment (continuous / widest across bursts)",
mode.label()
);
println!(" SNR dB | Eb/N0 dB | lag | continuous | burst | perfect clock");
println!(" -------|----------|-------|--------------|--------------|--------------");
let mut continuous = Vec::with_capacity(rungs.len());
let mut burst = Vec::with_capacity(rungs.len());
let mut perfect = Vec::with_capacity(rungs.len());
let mut pinned = 0usize;
for rung in rungs {
let mut cerr = 0u64;
let mut ccmp = 0u64;
let mut berr = 0u64;
let mut bcmp = 0u64;
let mut blag = 0usize;
let mut clag = 0usize;
for s in 0..SEEDS_PER_RUNG {
let seed = seed ^ (s as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
let ca = continuous_ber(mode, rung.snr_db, seed);
cerr += ca.errors as u64;
ccmp += ca.compared as u64;
clag = clag.max(ca.offset);
let (be, bn, bl) = burst_ber(mode, rung.snr_db, seed);
berr += be as u64;
bcmp += bn as u64;
blag = blag.max(bl);
}
let (be, bn) = (berr as usize, bcmp as usize);
let c = cerr as f64 / ccmp as f64;
let b = berr as f64 / bcmp as f64;
let p = perfect_clock_ber(mode, rung.snr_db, seed);
let head = format!(
" {:6.1} | {:8.1} | {:2} /{:2} | {:12.3e} | {:12.3e} | ",
rung.snr_db,
eb_n0_db(rung.snr_db, baud),
clag,
blag,
c,
b
);
match p {
Some(p) => println!("{head}{p:13.3e}"),
None => println!("{head}(no public FE)"),
}
pin(
mode,
rung.snr_db,
"continuous",
c,
cerr as usize,
ccmp as usize,
rung.continuous,
);
pin(mode, rung.snr_db, "burst", b, be, bn, rung.burst);
pinned += usize::from(rung.continuous.is_some()) + usize::from(rung.burst.is_some());
if let (Some(p), Some(ceiling)) = (p, rung.perfect_clock) {
pinned += 1;
assert!(
p <= ceiling,
"{}: perfect-clock BER at {} dB SNR is {p:.4e}, above the pinned \
ceiling {ceiling:.4e} — the tone correlator itself has regressed",
mode.label(),
rung.snr_db
);
}
continuous.push(c);
burst.push(b);
if let Some(p) = p {
perfect.push(p);
}
}
check_monotonic(mode, "continuous", &continuous);
check_monotonic(mode, "burst", &burst);
check_monotonic(mode, "perfect clock", &perfect);
let top = rungs.last().expect("non-empty ladder").snr_db;
let columns = [
Some(("continuous", *continuous.last().unwrap())),
Some(("burst", *burst.last().unwrap())),
perfect.last().map(|&p| ("perfect clock", p)),
];
for (name, v) in columns.into_iter().flatten() {
assert_eq!(
v,
0.0,
"{}: {name} BER is {v:.4e} at the cleanest rung ({top} dB) — \
must be exactly zero",
mode.label()
);
}
assert!(
pinned >= MIN_PINNED_CEILINGS,
"{}: only {pinned} of this ladder's ceilings were asserted, floor is \
{MIN_PINNED_CEILINGS}. A ladder whose columns are all `None` \
measures and prints exactly as much as one that is pinned, and \
proves nothing.",
mode.label()
);
}
fn pin(
mode: Mode,
snr_db: f64,
column: &str,
ber: f64,
errors: usize,
compared: usize,
ceiling: Option<f64>,
) {
if let Some(ceiling) = ceiling {
assert!(
ber <= ceiling,
"{}: {column} BER at {snr_db} dB SNR is {ber:.4e} ({errors} errors in \
{compared} bits), above the pinned ceiling {ceiling:.4e}",
mode.label()
);
}
}
fn check_monotonic(mode: Mode, column: &str, bers: &[f64]) {
for w in bers.windows(2) {
let slack = MONOTONE_SLACK + MONOTONE_REL_SLACK * w[0];
assert!(
w[1] <= w[0] + slack,
"{}: {column} BER is not monotonic in SNR — {:.4e} at the lower rung \
but {:.4e} at the higher one (slack {slack:.2e}); ladder {bers:?}",
mode.label(),
w[0],
w[1]
);
}
}
#[test]
fn raw_ber_bell_202() {
check_ladder(
Mode::Bell202,
&[
Rung {
snr_db: -3.0,
continuous: None, burst: None, perfect_clock: Some(4.0e-2),
},
Rung {
snr_db: -2.0,
continuous: None,
burst: None,
perfect_clock: Some(1.2e-2),
},
Rung {
snr_db: -1.0,
continuous: None,
burst: Some(2.0e-1),
perfect_clock: Some(2.0e-3),
},
Rung {
snr_db: 0.0,
continuous: None, burst: Some(4.0e-2), perfect_clock: Some(3.0e-4),
},
Rung {
snr_db: 1.0,
continuous: Some(1.0e-3),
burst: Some(1.0e-4),
perfect_clock: Some(1.0e-4),
},
Rung {
snr_db: 12.0,
continuous: Some(1.0e-4),
burst: Some(1.0e-4),
perfect_clock: Some(1.0e-4),
},
],
0x0BE1_1202,
);
}
#[test]
fn raw_ber_hf_300() {
check_ladder(
Mode::Hf300,
&[
Rung {
snr_db: -8.0,
continuous: None, burst: None,
perfect_clock: Some(8.0e-2),
},
Rung {
snr_db: -6.0,
continuous: None,
burst: Some(2.0e-1), perfect_clock: Some(2.5e-2),
},
Rung {
snr_db: -5.0,
continuous: None, burst: Some(6.0e-2), perfect_clock: Some(1.0e-2),
},
Rung {
snr_db: -4.0,
continuous: Some(1.0e-3),
burst: Some(1.0e-3),
perfect_clock: Some(3.0e-3),
},
Rung {
snr_db: -2.0,
continuous: Some(1.0e-4),
burst: Some(1.0e-4),
perfect_clock: Some(2.0e-4),
},
Rung {
snr_db: 12.0,
continuous: Some(1.0e-4),
burst: Some(1.0e-4),
perfect_clock: Some(1.0e-4),
},
],
0x0300_0300,
);
}
#[cfg(feature = "g3ruh")]
#[test]
fn raw_ber_g3ruh_9600() {
check_ladder(
Mode::G3ruh9600,
&[
Rung {
snr_db: -1.0,
continuous: None, burst: None,
perfect_clock: None, },
Rung {
snr_db: 1.0,
continuous: None, burst: Some(6.0e-2), perfect_clock: None,
},
Rung {
snr_db: 2.0,
continuous: Some(4.0e-2),
burst: Some(8.0e-3), perfect_clock: None,
},
Rung {
snr_db: 3.0,
continuous: Some(5.0e-4), burst: Some(5.0e-4), perfect_clock: None,
},
Rung {
snr_db: 5.0,
continuous: Some(1.0e-4),
burst: Some(1.0e-4),
perfect_clock: None,
},
Rung {
snr_db: 12.0,
continuous: Some(1.0e-4),
burst: Some(1.0e-4),
perfect_clock: None,
},
],
0x9600_9600,
);
}
#[test]
fn alignment_is_unambiguous_when_clean() {
assert!(
MODES.len() >= MIN_MODES,
"{} modes to align, floor is {MIN_MODES}",
MODES.len()
);
for &mode in MODES {
let a = clean_alignment(mode);
println!(
"{:17} clean alignment: lag {}, errors {}, runner-up {} of {} scored",
mode.label(),
a.offset,
a.errors,
a.runner_up_errors,
a.compared
);
assert_eq!(
a.errors,
0,
"{}: {} errors at {CLEAN_SNR_DB} dB with the best of {} lags",
mode.label(),
a.errors,
MAX_ALIGN + 1
);
assert!(
a.runner_up_errors * 4 >= a.compared,
"{}: alignment is ambiguous — winning lag {} scored {} errors but the \
runner-up scored only {} of {} bits; a real alignment spike leaves the \
runner-up near 50%",
mode.label(),
a.offset,
a.errors,
a.runner_up_errors,
a.compared
);
assert!(
a.offset < MAX_ALIGN,
"{}: winning lag {} sits on the edge of the {}-bit search window; the \
true minimum may lie outside it",
mode.label(),
a.offset,
MAX_ALIGN
);
assert!(
a.compared >= PAYLOAD_BITS - 4 * GUARD_BITS,
"{}: only {} of {PAYLOAD_BITS} payload bits were scored",
mode.label(),
a.compared
);
}
}
#[test]
#[ignore = "dense 0.5 dB-step BER sweep; slow. Run with -- --ignored --nocapture"]
fn ber_curve_fine_sweep() {
let spans: &[(Mode, f64, f64)] = &[
(Mode::Bell202, -5.0, 5.0),
(Mode::Hf300, -11.0, -1.0),
#[cfg(feature = "g3ruh")]
(Mode::G3ruh9600, -1.0, 9.0),
];
for &(mode, lo, hi) in spans {
println!("\n{} @ {SR_HZ} Hz — fine sweep, 0.5 dB steps", mode.label());
println!(" SNR dB | Eb/N0 dB | lag | continuous | burst | perfect clock");
println!(" -------|----------|-------|--------------|--------------|--------------");
let steps = ((hi - lo) / 0.5).round() as i32;
for i in 0..=steps {
let snr = lo + f64::from(i) * 0.5;
let ca = continuous_ber(mode, snr, 0xF14E_0001);
let (be, bn, blag) = burst_ber(mode, snr, 0xF14E_0001);
let p = perfect_clock_ber(mode, snr, 0xF14E_0001);
let head = format!(
" {:6.1} | {:8.1} | {:2} /{:2} | {:12.3e} | {:12.3e} | ",
snr,
eb_n0_db(snr, mode.baud_bps()),
ca.offset,
blag,
ca.errors as f64 / ca.compared as f64,
be as f64 / bn as f64
);
match p {
Some(p) => println!("{head}{p:13.3e}"),
None => println!("{head}(no public FE)"),
}
}
}
}
#[cfg(feature = "tnc")]
mod sensitivity {
use super::{Lcg, Mode, SR_HZ, mix, noise_peak};
use yodel::SampleRate;
use yodel::ax25::Address;
use yodel::tnc::{DefaultTncReceiver, TncConfig, TncReceiver, TncTransmitter};
const FRAMES: usize = 24;
const BISECT_STEPS: u32 = 6;
fn info(i: usize) -> [u8; 23] {
let mut buf = *b"yodel sensitivity 0000 ";
buf[18] = b'0' + ((i / 1000) % 10) as u8;
buf[19] = b'0' + ((i / 100) % 10) as u8;
buf[20] = b'0' + ((i / 10) % 10) as u8;
buf[21] = b'0' + (i % 10) as u8;
buf[22] = b'a' + (i % 26) as u8;
buf
}
fn recovered(mode: Mode, snr_db: f64, seed: u64) -> usize {
let sr = SampleRate::new(SR_HZ).unwrap();
let cfg = TncConfig::from_profile(sr, mode.profile()).unwrap();
let tx = TncTransmitter::new(cfg);
let dest = Address::new(b"APRS", 0).unwrap();
let peak = noise_peak(snr_db);
let mut rng = Lcg(seed);
let mut ok = 0usize;
for i in 0..FRAMES {
let src = Address::new(b"N0CALL", (i % 16) as u8).unwrap();
let payload = info(i);
let mut frame_buf = [0u8; 330];
let len = tx
.build_frame_raw(dest, src, &[], &payload, &mut frame_buf)
.unwrap();
let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
let mut got = false;
for s in tx.frame_samples_i16(&frame_buf[..len]) {
if let Some(frame) = rx.push_i16(mix(s, &mut rng, peak))
&& frame.info() == payload
{
got = true;
}
}
if got {
ok += 1;
}
}
ok
}
fn threshold_db(mode: Mode, lo: f64, hi: f64, seed: u64) -> f64 {
let majority = FRAMES / 2;
let at_lo = recovered(mode, lo, seed);
assert!(
at_lo < majority,
"{}: bracket floor {lo} dB already recovers {at_lo}/{FRAMES} — the 50% \
crossing is below the bracket, so bisection would return {lo} \
regardless of receiver quality",
mode.label()
);
let at_hi = recovered(mode, hi, seed);
assert!(
at_hi >= majority,
"{}: bracket ceiling {hi} dB recovers only {at_hi}/{FRAMES} — the 50% \
crossing is above the bracket",
mode.label()
);
let (mut lo, mut hi) = (lo, hi);
for _ in 0..BISECT_STEPS {
let mid = 0.5 * (lo + hi);
let ok = recovered(mode, mid, seed);
println!(" {:17} {mid:6.2} dB -> {ok:2}/{FRAMES}", mode.label());
if ok >= majority { hi = mid } else { lo = mid }
}
hi
}
fn check_threshold(mode: Mode, bracket: (f64, f64), pinned_db: f64, seed: u64) {
println!(
"\n{} @ {SR_HZ} Hz — 50% frame-recovery threshold, bisecting [{}, {}] dB",
mode.label(),
bracket.0,
bracket.1
);
let got = threshold_db(mode, bracket.0, bracket.1, seed);
println!(
" => threshold {got:.2} dB SNR (pinned: must stay at or below {pinned_db:.2} dB)"
);
assert!(
got <= pinned_db,
"{}: 50% frame-recovery threshold regressed to {got:.2} dB SNR; the \
pinned record is {pinned_db:.2} dB (lower is better)",
mode.label()
);
}
#[test]
fn sensitivity_threshold_bell_202() {
check_threshold(Mode::Bell202, (-8.0, 8.0), -2.0, 0x5E11_1202);
}
#[test]
fn sensitivity_threshold_hf_300() {
check_threshold(Mode::Hf300, (-14.0, 2.0), -6.5, 0x5E11_0300);
}
#[cfg(feature = "g3ruh")]
#[test]
fn sensitivity_threshold_g3ruh_9600() {
check_threshold(Mode::G3ruh9600, (-6.0, 10.0), 1.5, 0x5E11_9600);
}
}