Skip to main content

orion_sdr/modulate/
cw.rs

1// Copyright (c) 2025-2026 G & R Associates LLC
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::core::{Block, WorkReport};
5use crate::dsp::{Nco, mix_with_nco};
6use num_complex::Complex32 as C32;
7
8/// CW (keyed carrier) – envelope-shaped keyed NCO
9#[derive(Debug, Clone)]
10pub struct CwKeyedMod {
11    nco: Nco,
12    env: f32,
13    alpha_rise: f32,
14    alpha_fall: f32,
15    gain: f32,
16}
17
18impl CwKeyedMod {
19    /// `tone_hz`: CW tone frequency (baseband or RF, depending on usage)
20    /// `rise_ms`/`fall_ms`: envelope time constants (to avoid key clicks)
21    pub fn new(sample_rate: f32, tone_hz: f32, rise_ms: f32, fall_ms: f32) -> Self {
22        let tau_r = (rise_ms.max(0.1) * 1e-3) * sample_rate;
23        let tau_f = (fall_ms.max(0.1) * 1e-3) * sample_rate;
24        let alpha_r = (-1.0 / tau_r).exp();
25        let alpha_f = (-1.0 / tau_f).exp();
26        Self {
27            nco: Nco::new(tone_hz, sample_rate),
28            env: 0.0,
29            alpha_rise: alpha_r,
30            alpha_fall: alpha_f,
31            gain: 1.0,
32        }
33    }
34    pub fn set_gain(&mut self, g: f32) {
35        self.gain = g;
36    }
37}
38
39impl Block for CwKeyedMod {
40    type In = f32; // keying envelope 0..1 (you can derive this from audio or key events)
41    type Out = C32; // IQ
42
43    #[inline(always)]
44    fn process(&mut self, input: &[f32], output: &mut [C32]) -> WorkReport {
45        let n = input.len().min(output.len());
46        let mut i = 0;
47        let nn = n & !3;
48
49        while i < nn {
50            // 0
51            let tgt0 = input[i].clamp(0.0, 1.0);
52            self.env = if tgt0 >= self.env {
53                self.alpha_rise * self.env + (1.0 - self.alpha_rise) * tgt0
54            } else {
55                self.alpha_fall * self.env + (1.0 - self.alpha_fall) * tgt0
56            };
57            output[i] = mix_with_nco(C32::new(self.env * self.gain, 0.0), &mut self.nco);
58
59            // 1
60            let tgt1 = input[i + 1].clamp(0.0, 1.0);
61            self.env = if tgt1 >= self.env {
62                self.alpha_rise * self.env + (1.0 - self.alpha_rise) * tgt1
63            } else {
64                self.alpha_fall * self.env + (1.0 - self.alpha_fall) * tgt1
65            };
66            output[i + 1] = mix_with_nco(C32::new(self.env * self.gain, 0.0), &mut self.nco);
67
68            // 2
69            let tgt2 = input[i + 2].clamp(0.0, 1.0);
70            self.env = if tgt2 >= self.env {
71                self.alpha_rise * self.env + (1.0 - self.alpha_rise) * tgt2
72            } else {
73                self.alpha_fall * self.env + (1.0 - self.alpha_fall) * tgt2
74            };
75            output[i + 2] = mix_with_nco(C32::new(self.env * self.gain, 0.0), &mut self.nco);
76
77            // 3
78            let tgt3 = input[i + 3].clamp(0.0, 1.0);
79            self.env = if tgt3 >= self.env {
80                self.alpha_rise * self.env + (1.0 - self.alpha_rise) * tgt3
81            } else {
82                self.alpha_fall * self.env + (1.0 - self.alpha_fall) * tgt3
83            };
84            output[i + 3] = mix_with_nco(C32::new(self.env * self.gain, 0.0), &mut self.nco);
85
86            i += 4;
87        }
88        while i < n {
89            let tgt = input[i].clamp(0.0, 1.0);
90            self.env = if tgt >= self.env {
91                self.alpha_rise * self.env + (1.0 - self.alpha_rise) * tgt
92            } else {
93                self.alpha_fall * self.env + (1.0 - self.alpha_fall) * tgt
94            };
95            output[i] = mix_with_nco(C32::new(self.env * self.gain, 0.0), &mut self.nco);
96            i += 1;
97        }
98        WorkReport {
99            in_read: n,
100            out_written: n,
101        }
102    }
103}