fn unit_samples(sample_rate: f32, wpm: f32) -> f32 {
let unit_ms = 1200.0 / wpm.max(1.0);
(unit_ms * 1e-3) * sample_rate
}
const MORSE_TABLE: &[(char, &str)] = &[
('A', ".-"),
('B', "-..."),
('C', "-.-."),
('D', "-.."),
('E', "."),
('F', "..-."),
('G', "--."),
('H', "...."),
('I', ".."),
('J', ".---"),
('K', "-.-"),
('L', ".-.."),
('M', "--"),
('N', "-."),
('O', "---"),
('P', ".--."),
('Q', "--.-"),
('R', ".-."),
('S', "..."),
('T', "-"),
('U', "..-"),
('V', "...-"),
('W', ".--"),
('X', "-..-"),
('Y', "-.--"),
('Z', "--.."),
('0', "-----"),
('1', ".----"),
('2', "..---"),
('3', "...--"),
('4', "....-"),
('5', "....."),
('6', "-...."),
('7', "--..."),
('8', "---.."),
('9', "----."),
('.', ".-.-.-"),
(',', "--..--"),
('?', "..--.."),
('\'', ".----."),
('!', "-.-.--"),
('/', "-..-."),
('(', "-.--."),
(')', "-.--.-"),
('&', ".-..."),
(':', "---..."),
(';', "-.-.-."),
('=', "-...-"),
('+', ".-.-."),
('-', "-....-"),
('_', "..--.-"),
('"', ".-..-."),
('$', "...-..-"),
('@', ".--.-."),
];
fn char_to_morse(c: char) -> Option<&'static str> {
let upper = c.to_ascii_uppercase();
MORSE_TABLE
.iter()
.find(|(ch, _)| *ch == upper)
.map(|(_, pat)| *pat)
}
pub struct MorseEncoder {
sample_rate: f32,
wpm: f32,
jitter_pct: f32,
dash_weight: f32,
char_space: f32,
word_space: f32,
pub rng: u64,
}
impl MorseEncoder {
pub fn new(sample_rate: f32, wpm: f32) -> Self {
Self {
sample_rate,
wpm,
jitter_pct: 0.0,
dash_weight: 3.0,
char_space: 3.0,
word_space: 7.0,
rng: 0x853c_49e6_748f_ea9b,
}
}
pub fn with_jitter(mut self, pct: f32) -> Self {
self.jitter_pct = pct.clamp(0.0, 30.0);
self
}
pub fn with_dash_weight(mut self, w: f32) -> Self {
self.dash_weight = w.clamp(2.5, 3.5);
self
}
pub fn with_char_space(mut self, s: f32) -> Self {
self.char_space = s.clamp(2.5, 4.0);
self
}
pub fn with_word_space(mut self, s: f32) -> Self {
self.word_space = s.clamp(6.0, 9.0);
self
}
pub fn encode_text(&mut self, text: &str) -> Vec<f32> {
let unit = unit_samples(self.sample_rate, self.wpm);
let mut out = Vec::new();
let mut pending_gap: Option<f32> = None;
for c in text.chars() {
if c.is_ascii_whitespace() {
if pending_gap.is_some() || !out.is_empty() {
pending_gap = Some(self.word_space);
}
continue;
}
let pattern = match char_to_morse(c) {
Some(p) => p,
None => continue, };
if let Some(gap_units) = pending_gap.take() {
let n = self.jittered_samples(unit, gap_units);
out.extend(std::iter::repeat_n(0.0f32, n));
}
for (i, elem) in pattern.chars().enumerate() {
if i > 0 {
let n = self.jittered_samples(unit, 1.0);
out.extend(std::iter::repeat_n(0.0f32, n));
}
let units = match elem {
'.' => 1.0,
'-' => self.dash_weight,
_ => continue,
};
let n = self.jittered_samples(unit, units);
out.extend(std::iter::repeat_n(1.0f32, n));
}
pending_gap = Some(self.char_space);
}
out
}
fn jittered_samples(&mut self, unit: f32, units: f32) -> usize {
let nominal = unit * units;
if self.jitter_pct <= 0.0 {
return nominal.round() as usize;
}
let max_delta = unit * (self.jitter_pct / 100.0);
let r = self.xorshift_uniform(); let jittered = nominal + r * max_delta;
jittered.round().max(1.0) as usize
}
fn xorshift_uniform(&mut self) -> f32 {
self.rng ^= self.rng << 13;
self.rng ^= self.rng >> 7;
self.rng ^= self.rng << 17;
(self.rng >> 11) as f32 * (1.0 / (1u64 << 53) as f32) * 2.0 - 1.0
}
}