[fix](trx-cw): emit CW transition events
Restore empty events on tone edges so the frontend signal indicator updates again. Add synthetic-tone tests covering transitions and basic decoding. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -346,6 +346,7 @@ impl CwDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tone_on_at = now;
|
self.tone_on_at = now;
|
||||||
|
self.emit_event("");
|
||||||
} else if !detected && self.tone_on {
|
} else if !detected && self.tone_on {
|
||||||
// Tone just turned off
|
// Tone just turned off
|
||||||
self.tone_on = false;
|
self.tone_on = false;
|
||||||
@@ -366,6 +367,8 @@ impl CwDecoder {
|
|||||||
}
|
}
|
||||||
self.auto_detect_wpm();
|
self.auto_detect_wpm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.emit_event("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush pending character after long silence
|
// Flush pending character after long silence
|
||||||
@@ -379,7 +382,7 @@ impl CwDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_text(&mut self, text: &str) {
|
fn emit_event(&mut self, text: &str) {
|
||||||
self.events.push(CwEvent {
|
self.events.push(CwEvent {
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
wpm: self.wpm,
|
wpm: self.wpm,
|
||||||
@@ -388,6 +391,10 @@ impl CwDecoder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_text(&mut self, text: &str) {
|
||||||
|
self.emit_event(text);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_samples(&mut self, samples: &[f32]) -> Vec<CwEvent> {
|
pub fn process_samples(&mut self, samples: &[f32]) -> Vec<CwEvent> {
|
||||||
for &s in samples {
|
for &s in samples {
|
||||||
self.sample_buf[self.sample_idx] = s;
|
self.sample_buf[self.sample_idx] = s;
|
||||||
@@ -424,3 +431,60 @@ impl CwDecoder {
|
|||||||
self.events.clear();
|
self.events.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::CwDecoder;
|
||||||
|
|
||||||
|
fn tone_samples(sample_rate: u32, freq_hz: f32, ms: u32) -> Vec<f32> {
|
||||||
|
let len = (sample_rate as usize * ms as usize) / 1000;
|
||||||
|
let step = 2.0 * std::f32::consts::PI * freq_hz / sample_rate as f32;
|
||||||
|
(0..len).map(|i| (i as f32 * step).sin() * 0.8).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn silence_samples(sample_rate: u32, ms: u32) -> Vec<f32> {
|
||||||
|
vec![0.0; (sample_rate as usize * ms as usize) / 1000]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emits_signal_transition_events() {
|
||||||
|
let sample_rate = 48_000;
|
||||||
|
let mut decoder = CwDecoder::new(sample_rate);
|
||||||
|
decoder.set_auto(false);
|
||||||
|
decoder.set_wpm(15);
|
||||||
|
decoder.set_tone_hz(700);
|
||||||
|
|
||||||
|
let mut input = tone_samples(sample_rate, 700.0, 100);
|
||||||
|
input.extend(silence_samples(sample_rate, 500));
|
||||||
|
|
||||||
|
let events = decoder.process_samples(&input);
|
||||||
|
|
||||||
|
assert!(events
|
||||||
|
.iter()
|
||||||
|
.any(|evt| evt.text.is_empty() && evt.signal_on));
|
||||||
|
assert!(events
|
||||||
|
.iter()
|
||||||
|
.any(|evt| evt.text.is_empty() && !evt.signal_on));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_single_e_from_synthetic_tone() {
|
||||||
|
let sample_rate = 48_000;
|
||||||
|
let mut decoder = CwDecoder::new(sample_rate);
|
||||||
|
decoder.set_auto(false);
|
||||||
|
decoder.set_wpm(15);
|
||||||
|
decoder.set_tone_hz(700);
|
||||||
|
|
||||||
|
let mut input = tone_samples(sample_rate, 700.0, 100);
|
||||||
|
input.extend(silence_samples(sample_rate, 500));
|
||||||
|
|
||||||
|
let events = decoder.process_samples(&input);
|
||||||
|
let text: String = events
|
||||||
|
.iter()
|
||||||
|
.filter(|evt| !evt.text.is_empty())
|
||||||
|
.map(|evt| evt.text.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(text, "E");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user