Morse code
Morse code both ways, with accented Latin letters, Cyrillic and Greek tables and prosigns like `<AR>`; timing at any speed with Farnsworth spacing; a WAV of the keyed tone written by hand, and read back from its samples.
Version 1.0.0 · 6 functions · 1 crate
Functions
What use morse.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use morse_encode, morse_decode from morse (Quillo 0.6).
morse_encode(text: str, options: any) → stror an errorText as Morse: letters apart by a space, words by /. options: { alphabet: "latin" | "cyrillic" | "greek" }. <SOS> and the like are prosigns, sent as one.
morse_decode(code: str, options: any) → stror an errorMorse as text. Takes /, | or three spaces between words, and typographic dots and dashes. A sign no table has is an error naming it; with { lenient: true } it becomes U+FFFD.
morse_table(alphabet: str) → anyor an errorThe table of one alphabet: [{ letter, code, units }], and the prosigns with their meanings.
morse_timing(text: str, options: any) → anyor an errorHow a text is keyed: { dot_ms, units, seconds, signs, sequence }, sequence a list of [on, milliseconds]. options: { wpm, farnsworth, alphabet }. A dot is 1200/wpm ms; Farnsworth stretches the gaps by the ARRL's formula.
morse_wav(text: str, path: str, options: any) → anyor an errorA WAV of the text keyed as a tone: 16-bit mono PCM, each element ramped over 5 ms so it does not click. options: { wpm, farnsworth, frequency, sample_rate, volume, alphabet }. Gives { path, seconds, samples, bytes }.
morse_read_wav(path: str, options: any) → anyor an errorMorse read from a WAV of a clean tone: the envelope is followed every 2 ms, the dot length found from the shortest marks, and the signs decoded. Gives { text, code, dot_ms, wpm, unknown }. Noisy recordings are beyond it.
Get it
$ quillo install morse # morse.qwer into this folder$ quillo install morse --example # and its example, morse.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/morse.qwer$ curl -O https://quillolang.org/plugins/files/morse.qweThen, in your program, all of it or only what you call:
use morse.qweruse morse_encode, morse_decode from morse // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// Morse code: a text sent and read back, an accented or Cyrillic word, how long it takes to key// at a speed, and a WAV of the tone written to disk, played by a browser and decoded again.//// quillo run morse.qwe// then open http://localhost:8080/encode?text=Hello+world, /decode, /timing, /sound.wav and /hearduse "morse.qwer"route GET /encode ? { text: str = "SOS HELP" } = { text: text, code: morse_encode(text, {})? }route GET /decode = { plain: morse_decode("-- --- .-. ... . / -.-. --- -.. .", {})?, dashes: morse_decode("••• ———", { lenient: true })?, russian: morse_decode(morse_encode("Привет мир", { alphabet: "cyrillic" })?, { alphabet: "cyrillic" })?, greek: morse_encode("Σωκράτης", { alphabet: "greek" })?, accented: morse_encode("Ñandú Ü", {})?, prosign: morse_encode("CQ <AR>", {})?, wrong: morse_decode("...... ---", {}).error, no_code: morse_encode("price: 5€", {}).error,}// The word PARIS is 50 units, so 20 words a minute is exactly a 60 ms dot.route GET /timing = { paris: morse_timing("PARIS", { wpm: 20 })?.units, dot_ms: morse_timing("PARIS", { wpm: 20 })?.dot_ms, seconds: morse_timing("SOS", { wpm: 20 })?.seconds, slow: morse_timing("PARIS", { wpm: 18, farnsworth: 5 })?.word_gap_ms, too_fast: morse_timing("E", { wpm: 10, farnsworth: 20 }).error,}let path = "data/morse-example.wav"route GET /sound.wav { write_file("data/.keep", "") morse_wav("CQ DE QUILLO", path, { wpm: 18, frequency: 700.0 })? file(path)}// Written, then read from the samples alone.route GET /heard { write_file("data/.keep", "") let made = morse_wav("HELLO WORLD 73", path, { wpm: 25 })? { made: made.seconds, heard: morse_read_wav(path, {})? }}route GET /not-a-wav { write_file(path + ".txt", "nothing") { error: morse_read_wav(path + ".txt", {}).error }}$ quillo run morse.qwe$ curl 'localhost:8080/encode?text=Hello+world'$ curl 'localhost:8080/decode'$ curl 'localhost:8080/timing'$ curl 'localhost:8080/heard'$ curl 'localhost:8080/not-a-wav'Source
//! Morse code both ways: Latin with accented letters, Cyrillic and Greek tables, prosigns, timing in units and milliseconds with Farnsworth spacing, and a WAV of the keyed tone written and read back.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};const LATIN: &[(&str, &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", "----."), (".", ".-.-.-"), (",", "--..--"), ("?", "..--.."), ("'", ".----."), ("!", "-.-.--"), ("/", "-..-."), ("(", "-.--."), (")", "-.--.-"), ("&", ".-..."), (":", "---..."), (";", "-.-.-."), ("=", "-...-"), ("+", ".-.-."), ("-", "-....-"), ("_", "..--.-"), ("\"", ".-..-."), ("$", "...-..-"), ("@", ".--.-."),];// Accented letters: several share a code, so the first of each code is what decoding gives.const ACCENTED: &[(&str, &str)] = &[ ("À", ".--.-"), ("Å", ".--.-"), ("Ä", ".-.-"), ("Æ", ".-.-"), ("Ą", ".-.-"), ("Ç", "-.-.."), ("Ć", "-.-.."), ("Ĉ", "-.-.."), ("É", "..-.."), ("Đ", "..-.."), ("Ę", "..-.."), ("Ð", "..--."), ("È", ".-..-"), ("Ł", ".-..-"), ("Ĝ", "--.-."), ("Ĥ", "----"), ("Š", "----"), ("Ĵ", ".---."), ("Ñ", "--.--"), ("Ń", "--.--"), ("Ö", "---."), ("Ó", "---."), ("Ø", "---."), ("Ś", "...-..."), ("Ŝ", "...-."), ("Þ", ".--.."), ("Ü", "..--"), ("Ŭ", "..--"), ("Ź", "--..-."), ("Ż", "--..-"),];const CYRILLIC: &[(&str, &str)] = &[ ("А", ".-"), ("Б", "-..."), ("В", ".--"), ("Г", "--."), ("Д", "-.."), ("Е", "."), ("Ж", "...-"), ("З", "--.."), ("И", ".."), ("Й", ".---"), ("К", "-.-"), ("Л", ".-.."), ("М", "--"), ("Н", "-."), ("О", "---"), ("П", ".--."), ("Р", ".-."), ("С", "..."), ("Т", "-"), ("У", "..-"), ("Ф", "..-."), ("Х", "...."), ("Ц", "-.-."), ("Ч", "---."), ("Ш", "----"), ("Щ", "--.-"), ("Ъ", "--.--"), ("Ы", "-.--"), ("Ь", "-..-"), ("Э", "..-.."), ("Ю", "..--"), ("Я", ".-.-"), ("Ё", "."),];const GREEK: &[(&str, &str)] = &[ ("Α", ".-"), ("Β", "-..."), ("Γ", "--."), ("Δ", "-.."), ("Ε", "."), ("Ζ", "--.."), ("Η", "...."), ("Θ", "-.-."), ("Ι", ".."), ("Κ", "-.-"), ("Λ", ".-.."), ("Μ", "--"), ("Ν", "-."), ("Ξ", "-..-"), ("Ο", "---"), ("Π", ".--."), ("Ρ", ".-."), ("Σ", "..."), ("Τ", "-"), ("Υ", "-.--"), ("Φ", "..-."), ("Χ", "----"), ("Ψ", "--.-"), ("Ω", ".--"),];const PROSIGNS: &[(&str, &str, &str)] = &[ ("SOS", "...---...", "distress"), ("AR", ".-.-.", "end of message"), ("SK", "...-.-", "end of contact"), ("BT", "-...-", "new section"), ("KN", "-.--.", "go ahead, named station only"), ("AS", ".-...", "wait"), ("HH", "........", "correction"), ("SN", "...-.", "understood"), ("CT", "-.-.-", "start of message"),];// Greek's accents and final sigma, and letters written in a Latin form Morse has no code for.fn plain(c: char) -> Option<char> { Some(match c { 'ς' | 'Σ' => 'Σ', 'Ά' | 'ά' => 'Α', 'Έ' | 'έ' => 'Ε', 'Ή' | 'ή' => 'Η', 'Ί' | 'ί' | 'ϊ' | 'ΐ' => 'Ι', 'Ό' | 'ό' => 'Ο', 'Ύ' | 'ύ' | 'ϋ' | 'ΰ' => 'Υ', 'Ώ' | 'ώ' => 'Ω', 'Á' | 'Â' | 'Ã' | 'Ā' => 'A', 'Ê' | 'Ë' | 'Ē' => 'E', 'Í' | 'Î' | 'Ï' | 'Ī' => 'I', 'Ô' | 'Õ' | 'Ō' => 'O', 'Ú' | 'Û' | 'Ū' => 'U', 'Ý' | 'Ÿ' => 'Y', 'ß' => 'S', _ => return None, })}fn tables(alphabet: &str) -> Result<Vec<(&'static str, &'static str)>, String> { let mut t: Vec<(&str, &str)> = match alphabet { "latin" | "" => [ACCENTED, CYRILLIC, GREEK].concat(), "cyrillic" => [CYRILLIC, ACCENTED, GREEK].concat(), "greek" => [GREEK, ACCENTED, CYRILLIC].concat(), o => return Err(format!("no Morse alphabet `{}`; there are latin, cyrillic and greek", o)), }; if alphabet == "latin" || alphabet.is_empty() { let mut all = LATIN.to_vec(); all.append(&mut t); return Ok(all); } let first = alphabet == "cyrillic" || alphabet == "greek"; let n = if alphabet == "cyrillic" { CYRILLIC.len() } else { GREEK.len() }; if first { let mut all: Vec<(&str, &str)> = t[..n].to_vec(); all.extend_from_slice(LATIN); all.extend_from_slice(&t[n..]); return Ok(all); } Ok(t)}fn alphabet_of(options: &Value) -> Result<String, String> { Ok(options.get("alphabet").and_then(|v| v.as_str()).unwrap_or("latin").to_lowercase())}fn encode_words(text: &str, alphabet: &str) -> Result<Vec<Vec<String>>, String> { let table = tables(alphabet)?; let mut words = vec![]; for word in text.split_whitespace() { let mut letters = vec![]; let up: Vec<char> = word.chars().collect(); let mut i = 0; while i < up.len() { if up[i] == '<' { if let Some(end) = up[i..].iter().position(|c| *c == '>') { let name: String = up[i + 1..i + end].iter().collect::<String>().to_uppercase(); let p = PROSIGNS.iter().find(|p| p.0 == name).ok_or(format!("no prosign <{}>", name))?; letters.push(p.1.to_string()); i += end + 1; continue; } } let raw = up[i]; let one: String = raw.to_uppercase().collect(); let found = table.iter().find(|e| e.0 == one).map(|e| e.1).or_else(|| plain(raw).or(one.chars().next().and_then(plain)).and_then(|c| table.iter().find(|e| e.0 == c.to_string()).map(|e| e.1))); // ß is written SS, and letters of more than one code each go in turn. if raw == 'ß' { letters.push("...".to_string()); letters.push("...".to_string()); } else { letters.push(found.ok_or(format!("`{}` has no Morse code (character {} of the text)", raw, i + 1))?.to_string()); } i += 1; } words.push(letters); } Ok(words)}/// Text as Morse: letters apart by a space, words by ` / `. `options`: { alphabet: "latin" | "cyrillic" | "greek" }. `<SOS>` and the like are prosigns, sent as one.pub fn morse_encode(text: &str, options: serde_json::Value) -> Result<String, String> { let words = encode_words(text, &alphabet_of(&options)?)?; if words.is_empty() { return Err("nothing to send".into()); } Ok(words.iter().map(|w| w.join(" ")).collect::<Vec<_>>().join(" / "))}fn decode_with(code: &str, alphabet: &str) -> Result<(String, Vec<String>), String> { let table = tables(alphabet)?; let spaced = code.replace('\u{2022}', ".").replace(['\u{2013}', '\u{2014}', '\u{2212}', '_'], "-").replace('|', "/"); let mut out = String::new(); let mut unknown = vec![]; let lines: Vec<String> = spaced.trim().replace(" ", " / ").split('/').map(|w| w.trim().to_string()).filter(|w| !w.is_empty()).collect(); for (i, word) in lines.iter().enumerate() { if i > 0 { out.push(' '); } for sign in word.split_whitespace() { if !sign.chars().all(|c| c == '.' || c == '-') { return Err(format!("`{}` is not Morse: only dots, dashes, spaces and / ", sign)); } match table.iter().find(|e| e.1 == sign) { Some(e) => out.push_str(e.0), None => match PROSIGNS.iter().find(|p| p.1 == sign) { Some(p) => out.push_str(&format!("<{}>", p.0)), None => { out.push('\u{FFFD}'); unknown.push(sign.to_string()); } }, } } } Ok((out, unknown))}/// Morse as text. Takes ` / `, `|` or three spaces between words, and typographic dots and dashes. A sign no table has is an error naming it; with `{ lenient: true }` it becomes U+FFFD.pub fn morse_decode(code: &str, options: serde_json::Value) -> Result<String, String> { let (text, unknown) = decode_with(code, &alphabet_of(&options)?)?; if !unknown.is_empty() && !options.get("lenient").and_then(|v| v.as_bool()).unwrap_or(false) { return Err(format!("`{}` is no letter in Morse", unknown[0])); } Ok(text)}/// The table of one alphabet: [{ letter, code, units }], and the prosigns with their meanings.pub fn morse_table(alphabet: &str) -> Result<Value, String> { let list: &[(&str, &str)] = match alphabet { "latin" | "" => LATIN, "accented" => ACCENTED, "cyrillic" => CYRILLIC, "greek" => GREEK, o => return Err(format!("no Morse alphabet `{}`; there are latin, accented, cyrillic and greek", o)), }; let row = |(l, c): &(&str, &str)| json!({ "letter": l, "code": c, "units": units_of(c) }); Ok(json!({ "letters": list.iter().map(row).collect::<Vec<_>>(), "prosigns": PROSIGNS.iter().map(|(n, c, m)| json!({ "name": n, "code": c, "means": m })).collect::<Vec<_>>(), }))}fn units_of(code: &str) -> i64 { let n = code.chars().count() as i64; code.chars().map(|c| if c == '.' { 1 } else { 3 }).sum::<i64>() + n - 1}// Key-down and key-up lengths in dot units: (on, units), with gaps weighted as Farnsworth asks.fn keying(words: &[Vec<String>], letter_gap: f64, word_gap: f64) -> Vec<(bool, f64)> { let mut seq = vec![]; for (wi, w) in words.iter().enumerate() { if wi > 0 { seq.push((false, word_gap)); } for (li, l) in w.iter().enumerate() { if li > 0 { seq.push((false, letter_gap)); } for (si, c) in l.chars().enumerate() { if si > 0 { seq.push((false, 1.0)); } seq.push((true, if c == '.' { 1.0 } else { 3.0 })); } } } seq}struct Speed { wpm: f64, effective: f64, dot_ms: f64, letter_gap: f64, word_gap: f64,}// PARIS is 50 units: 1200 / wpm ms a dot. Farnsworth keeps the letters fast and stretches the gaps:// of the 50 units 31 are in letters and 19 in gaps (12 letter gaps of 3, ... ARRL's formula).fn speed_of(options: &Value) -> Result<Speed, String> { let wpm = options.get("wpm").and_then(|v| v.as_f64()).unwrap_or(20.0); if !(wpm > 0.0 && wpm <= 200.0) { return Err(format!("wpm is 1 to 200, not {}", wpm)); } let effective = options.get("farnsworth").and_then(|v| v.as_f64()).unwrap_or(wpm); if !(effective > 0.0) || effective > wpm { return Err(format!("Farnsworth speed {} must be above 0 and no more than the {} wpm of the letters", effective, wpm)); } let dot_ms = 1200.0 / wpm; let (mut letter_gap, mut word_gap) = (3.0, 7.0); if effective < wpm { let delay_s = (60.0 * wpm - 37.2 * effective) / (wpm * effective); let unit_gap_ms = delay_s * 1000.0 / 19.0; letter_gap = 3.0 * unit_gap_ms / dot_ms; word_gap = 7.0 * unit_gap_ms / dot_ms; } Ok(Speed { wpm, effective, dot_ms, letter_gap, word_gap })}/// How a text is keyed: { dot_ms, units, seconds, signs, sequence }, sequence a list of [on, milliseconds]. `options`: { wpm, farnsworth, alphabet }. A dot is 1200/wpm ms; Farnsworth stretches the gaps by the ARRL's formula.pub fn morse_timing(text: &str, options: serde_json::Value) -> Result<Value, String> { let sp = speed_of(&options)?; let words = encode_words(text, &alphabet_of(&options)?)?; let seq = keying(&words, sp.letter_gap, sp.word_gap); let units: f64 = seq.iter().map(|s| s.1).sum(); let round = |x: f64| (x * 100.0).round() / 100.0; Ok(json!({ "wpm": sp.wpm, "effective_wpm": sp.effective, "dot_ms": round(sp.dot_ms), "letter_gap_ms": round(sp.letter_gap * sp.dot_ms), "word_gap_ms": round(sp.word_gap * sp.dot_ms), "units": round(units), "seconds": round(units * sp.dot_ms / 1000.0), "elements": seq.iter().filter(|s| s.0).count(), "sequence": seq.iter().map(|(on, u)| json!([on, round(u * sp.dot_ms)])).collect::<Vec<_>>(), }))}fn wav_header(samples: usize, rate: u32) -> Vec<u8> { let bytes = (samples * 2) as u32; let mut h = Vec::with_capacity(44); h.extend_from_slice(b"RIFF"); h.extend_from_slice(&(36 + bytes).to_le_bytes()); h.extend_from_slice(b"WAVEfmt "); h.extend_from_slice(&16u32.to_le_bytes()); h.extend_from_slice(&1u16.to_le_bytes()); h.extend_from_slice(&1u16.to_le_bytes()); h.extend_from_slice(&rate.to_le_bytes()); h.extend_from_slice(&(rate * 2).to_le_bytes()); h.extend_from_slice(&2u16.to_le_bytes()); h.extend_from_slice(&16u16.to_le_bytes()); h.extend_from_slice(b"data"); h.extend_from_slice(&bytes.to_le_bytes()); h}/// A WAV of the text keyed as a tone: 16-bit mono PCM, each element ramped over 5 ms so it does not click. `options`: { wpm, farnsworth, frequency, sample_rate, volume, alphabet }. Gives { path, seconds, samples, bytes }.pub fn morse_wav(text: &str, path: &str, options: serde_json::Value) -> Result<Value, String> { let sp = speed_of(&options)?; let freq = options.get("frequency").and_then(|v| v.as_f64()).unwrap_or(600.0); let rate = options.get("sample_rate").and_then(|v| v.as_i64()).unwrap_or(8000); let vol = options.get("volume").and_then(|v| v.as_f64()).unwrap_or(0.8); if !(4000..=192000).contains(&rate) { return Err(format!("sample_rate is 4000 to 192000, not {}", rate)); } if !(freq > 20.0 && freq < rate as f64 / 2.0) { return Err(format!("a frequency of {} Hz cannot be written at {} samples a second", freq, rate)); } if !(0.0..=1.0).contains(&vol) { return Err(format!("volume is 0 to 1, not {}", vol)); } let words = encode_words(text, &alphabet_of(&options)?)?; let seq = keying(&words, sp.letter_gap, sp.word_gap); let per_ms = rate as f64 / 1000.0; let total_ms: f64 = seq.iter().map(|s| s.1 * sp.dot_ms).sum(); if total_ms > 3_600_000.0 { return Err("that is over an hour of Morse".into()); } let mut pcm: Vec<i16> = Vec::with_capacity((total_ms * per_ms) as usize + 8); let ramp = ((5.0 * per_ms) as usize).max(1); let step = std::f64::consts::TAU * freq / rate as f64; for (on, u) in &seq { let n = (u * sp.dot_ms * per_ms).round() as usize; for i in 0..n { if !*on { pcm.push(0); continue; } let edge = i.min(n - 1 - i); let env = if edge < ramp { 0.5 - 0.5 * (std::f64::consts::PI * edge as f64 / ramp as f64).cos() } else { 1.0 }; pcm.push(((i as f64 * step).sin() * env * vol * 32767.0) as i16); } } let mut out = wav_header(pcm.len(), rate as u32); for s in &pcm { out.extend_from_slice(&s.to_le_bytes()); } std::fs::write(path, &out).map_err(|e| format!("cannot write {}: {}", path, e))?; Ok(json!({ "path": path, "seconds": (pcm.len() as f64 / rate as f64 * 1000.0).round() / 1000.0, "samples": pcm.len(), "bytes": out.len(), "sample_rate": rate, "frequency": freq, "wpm": sp.wpm }))}fn read_wav(path: &str) -> Result<(Vec<f64>, u32), String> { let b = std::fs::read(path).map_err(|e| format!("cannot read {}: {}", path, e))?; if b.len() < 12 || &b[0..4] != b"RIFF" || &b[8..12] != b"WAVE" { return Err(format!("{} is not a WAV file", path)); } let (mut at, mut fmt, mut data) = (12usize, None, None); while at + 8 <= b.len() { let id = &b[at..at + 4]; let len = u32::from_le_bytes([b[at + 4], b[at + 5], b[at + 6], b[at + 7]]) as usize; let body = at + 8; let end = (body + len).min(b.len()); if id == b"fmt " && end - body >= 16 { fmt = Some(&b[body..end]); } else if id == b"data" { data = Some(&b[body..end]); } at = body + len + (len & 1); } let fmt = fmt.ok_or("the WAV has no fmt chunk")?; let data = data.ok_or("the WAV has no data chunk")?; let kind = u16::from_le_bytes([fmt[0], fmt[1]]); let channels = u16::from_le_bytes([fmt[2], fmt[3]]) as usize; let rate = u32::from_le_bytes([fmt[4], fmt[5], fmt[6], fmt[7]]); let bits = u16::from_le_bytes([fmt[14], fmt[15]]); if kind != 1 || !(bits == 16 || bits == 8) || channels == 0 { return Err(format!("only 8 or 16-bit PCM can be read, this is format {} at {} bits", kind, bits)); } let frame = channels * bits as usize / 8; let pcm = data.chunks_exact(frame).map(|f| if bits == 16 { i16::from_le_bytes([f[0], f[1]]) as f64 / 32768.0 } else { (f[0] as f64 - 128.0) / 128.0 }).collect(); Ok((pcm, rate))}/// Morse read from a WAV of a clean tone: the envelope is followed every 2 ms, the dot length found from the shortest marks, and the signs decoded. Gives { text, code, dot_ms, wpm, unknown }. Noisy recordings are beyond it.pub fn morse_read_wav(path: &str, options: serde_json::Value) -> Result<Value, String> { let (pcm, rate) = read_wav(path)?; let win = ((rate as usize) / 500).max(1); let env: Vec<f64> = pcm.chunks(win).map(|c| c.iter().map(|x| x.abs()).fold(0.0, f64::max)).collect(); let peak = env.iter().cloned().fold(0.0, f64::max); if peak < 0.01 { return Err("the WAV is silent".into()); } let ms = win as f64 * 1000.0 / rate as f64; let mut runs: Vec<(bool, f64)> = vec![]; for e in &env { let on = *e > peak * 0.3; match runs.last_mut() { Some(r) if r.0 == on => r.1 += ms, _ => runs.push((on, ms)), } } while runs.first().map(|r| !r.0).unwrap_or(false) { runs.remove(0); } while runs.last().map(|r| !r.0).unwrap_or(false) { runs.pop(); } let marks: Vec<f64> = runs.iter().filter(|r| r.0).map(|r| r.1).collect(); if marks.is_empty() { return Err("no tone in the WAV".into()); } let shortest = marks.iter().cloned().fold(f64::MAX, f64::min); let longest = marks.iter().cloned().fold(0.0, f64::max); // Only dashes or only dots: a mark is a dash when three of the shortest gap fit it. let gap_min = runs.iter().filter(|r| !r.0).map(|r| r.1).fold(f64::MAX, f64::min); let dot = if longest > shortest * 2.0 { shortest } else if gap_min * 2.0 < shortest { gap_min } else { shortest }; let mut code = String::new(); for (on, len) in &runs { let u = len / dot; if *on { code.push(if u < 2.0 { '.' } else { '-' }); } else if u >= 5.0 { code.push_str(" / "); } else if u >= 2.0 { code.push(' '); } } let (text, unknown) = decode_with(&code, &alphabet_of(&options)?)?; Ok(json!({ "text": text, "code": code, "dot_ms": dot.round(), "wpm": (1200.0 / dot).round(), "unknown": unknown, "sample_rate": rate }))}Thread 0 posts
On Morse code: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.