Text

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

#morse#radio#audio#encoding

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 error

Text 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 error

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.

morse_table(alphabet: str) → anyor an error

The table of one alphabet: [{ letter, code, units }], and the prosigns with their meanings.

morse_timing(text: str, options: any) → anyor an error

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.

morse_wav(text: str, path: str, options: any) → anyor an error

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 }.

morse_read_wav(path: str, options: any) → anyor an error

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.

Get it

$ quillo install morse               # morse.qwer into this folder$ quillo install morse --example     # and its example, morse.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/morse.qwer$ curl -O https://quillolang.org/plugins/files/morse.qwe

Then, in your program, all of it or only what you call:

main.qwe
use morse.qweruse morse_encode, morse_decode from morse      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

morse.qwe
// 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.qwer
//! 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
411Useful? Like it, or add to its thread below.

Thread 0 posts

On Morse code: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.