]> code.octet-stream.net Git - m17rt/blobdiff - m17core/src/modem.rs
Address some clippy lints
[m17rt] / m17core / src / modem.rs
index af36ab4a88db3b38df0615a60ee2add25313590e..3d4890ad857376aba09d28ad46630be608ee698f 100644 (file)
@@ -1,5 +1,10 @@
-use crate::decode::{parse_lsf, parse_stream, sync_burst_correlation, SyncBurst, SYNC_THRESHOLD};
-use crate::protocol::Frame;
+use crate::decode::{
+    parse_lsf, parse_packet, parse_stream, sync_burst_correlation, SyncBurst, SYNC_THRESHOLD,
+};
+use crate::encode::{
+    encode_lsf, encode_packet, encode_stream, generate_end_of_transmission, generate_preamble,
+};
+use crate::protocol::{Frame, LsfFrame, PacketFrame, StreamFrame};
 use crate::shaping::RRC_48K;
 use log::debug;
 
 use crate::shaping::RRC_48K;
 use log::debug;
 
@@ -22,8 +27,10 @@ pub struct SoftDemodulator {
     candidate: Option<DecodeCandidate>,
     /// How many samples have we received?
     sample: u64,
     candidate: Option<DecodeCandidate>,
     /// How many samples have we received?
     sample: u64,
-    /// Remaining samples to ignore so once we already parse a frame we flush it out in full
-    suppress: u16,
+    /// Remaining samples to read in before attempting to decode the current candidate
+    samples_until_decode: Option<u16>,
+    /// Do we think there is a data carrier, i.e., channel in use? If so, at what sample does it expire?
+    dcd: Option<u64>,
 }
 
 impl SoftDemodulator {
 }
 
 impl SoftDemodulator {
@@ -35,7 +42,26 @@ impl SoftDemodulator {
             rx_cursor: 0,
             candidate: None,
             sample: 0,
             rx_cursor: 0,
             candidate: None,
             sample: 0,
-            suppress: 0,
+            samples_until_decode: None,
+            dcd: None,
+        }
+    }
+}
+
+impl SoftDemodulator {
+    fn dcd_until(&mut self, end_sample: u64) {
+        if self.dcd.is_none() {
+            debug!("SoftDemodulator DCD on");
+        }
+        self.dcd = Some(end_sample);
+    }
+
+    fn check_dcd(&mut self) {
+        if let Some(end_sample) = self.dcd {
+            if self.sample > end_sample {
+                self.dcd = None;
+                debug!("SoftDemodulator DCD off");
+            }
         }
     }
 }
         }
     }
 }
@@ -54,18 +80,67 @@ impl Demodulator for SoftDemodulator {
         self.rx_cursor = (self.rx_cursor + 1) % 1920;
 
         self.sample += 1;
         self.rx_cursor = (self.rx_cursor + 1) % 1920;
 
         self.sample += 1;
+        self.check_dcd();
+
+        if let Some(samples_until_decode) = self.samples_until_decode {
+            let sud = samples_until_decode - 1;
+            if sud > 0 {
+                self.samples_until_decode = Some(sud);
+                return None;
+            }
+            self.samples_until_decode = None;
 
 
-        if self.suppress > 0 {
-            self.suppress -= 1;
-            return None;
+            if let Some(c) = self.candidate.take() {
+                // we have capacity for 192 symbols * 10 upsamples
+                // we have calculated that the ideal sample point for 192nd symbol is right on the edge
+                // so take samples from the 10th slot all the way through.
+                let start_idx = self.rx_cursor + 1920 + 9;
+                let mut pkt_samples = [0f32; 192];
+                for i in 0..192 {
+                    let rx_idx = (start_idx + i * 10) % 1920;
+                    pkt_samples[i] = (self.rx_win[rx_idx] - c.shift) / c.gain;
+                }
+                match c.burst {
+                    SyncBurst::Lsf => {
+                        if let Some(frame) = parse_lsf(&pkt_samples) {
+                            return Some(Frame::Lsf(frame));
+                        }
+                    }
+                    SyncBurst::Bert => {
+                        // TODO: BERT
+                    }
+                    SyncBurst::Stream => {
+                        if let Some(frame) = parse_stream(&pkt_samples) {
+                            return Some(Frame::Stream(frame));
+                        }
+                    }
+                    SyncBurst::Packet => {
+                        if let Some(frame) = parse_packet(&pkt_samples) {
+                            return Some(Frame::Packet(frame));
+                        }
+                    }
+                    SyncBurst::Preamble | SyncBurst::EndOfTransmission => {
+                        // should never be chosen as a candidate
+                    }
+                }
+            }
         }
 
         }
 
-        let mut burst_window = [0f32; 71];
-        for i in 0..71 {
-            let c = (self.rx_cursor + i) % 1920;
+        let mut burst_window = [0f32; 8];
+        for i in 0..8 {
+            let c = (self.rx_cursor + 1920 - 1 - ((7 - i) * 10)) % 1920;
             burst_window[i] = self.rx_win[c];
         }
 
             burst_window[i] = self.rx_win[c];
         }
 
+        for burst in [SyncBurst::Preamble, SyncBurst::EndOfTransmission] {
+            let (diff, _, _) = sync_burst_correlation(burst.target(), &burst_window);
+            if diff < SYNC_THRESHOLD {
+                // arbitrary choice, 240 samples = 5ms
+                // these bursts keep repeating so it will keep pushing out the DCD end time
+                self.dcd_until(self.sample + 240);
+            }
+        }
+
         for burst in [
             SyncBurst::Lsf,
             SyncBurst::Bert,
         for burst in [
             SyncBurst::Lsf,
             SyncBurst::Bert,
@@ -98,43 +173,17 @@ impl Demodulator for SoftDemodulator {
                     .map(|c| c.burst == burst)
                     .unwrap_or(false)
             {
                     .map(|c| c.burst == burst)
                     .unwrap_or(false)
             {
-                if let Some(c) = self.candidate.take() {
-                    let start_idx = self.rx_cursor + 1920 - (c.age as usize);
-                    let start_sample = self.sample - c.age as u64;
-                    let mut pkt_samples = [0f32; 192];
-                    for i in 0..192 {
-                        let rx_idx = (start_idx + i * 10) % 1920;
-                        pkt_samples[i] = (self.rx_win[rx_idx] - c.shift) / c.gain;
-                    }
-                    match c.burst {
-                        SyncBurst::Lsf => {
-                            debug!(
-                                "Found LSF at sample {} diff {} max {} shift {}",
-                                start_sample, c.diff, c.gain, c.shift
-                            );
-                            if let Some(frame) = parse_lsf(&pkt_samples) {
-                                self.suppress = 191 * 10;
-                                return Some(Frame::Lsf(frame));
-                            }
-                        }
-                        SyncBurst::Bert => {
-                            debug!("Found BERT at sample {} diff {}", start_sample, c.diff);
-                        }
-                        SyncBurst::Stream => {
-                            debug!(
-                                "Found STREAM at sample {} diff {} max {} shift {}",
-                                start_sample, c.diff, c.gain, c.shift
-                            );
-                            if let Some(frame) = parse_stream(&pkt_samples) {
-                                self.suppress = 191 * 10;
-                                return Some(Frame::Stream(frame));
-                            }
-                        }
-                        SyncBurst::Packet => {
-                            debug!("Found PACKET at sample {} diff {}", start_sample, c.diff)
-                        }
-                    }
-                }
+                // wait until the rest of the frame is in the buffer
+                let c = self.candidate.as_ref().unwrap();
+                self.samples_until_decode = Some((184 * 10) - (c.age as u16));
+                debug!(
+                    "Found {:?} at sample {} diff {}",
+                    c.burst,
+                    self.sample - c.age as u64,
+                    c.diff
+                );
+                // After any of these frame types you would expect to see a full EOT
+                self.dcd_until(self.sample + 1920 + 1920);
             }
         }
 
             }
         }
 
@@ -152,6 +201,314 @@ impl Default for SoftDemodulator {
     }
 }
 
     }
 }
 
+pub trait Modulator {
+    /// Inform the modulator how many samples remain pending for output and latency updates.
+    ///
+    /// For the buffer between `Modulator` and the process which is supplying samples to the
+    /// output sound card, `samples_to_play` is the number of bytes which the modulator has
+    /// provided that have not yet been picked up, and `capacity` is the maximum size we can
+    /// fill this particular buffer, i.e., maximum number of samples.
+    ///
+    /// Furthermore we attempt to track and account for the latency between the output
+    /// soundcard callback, and when those samples will actually be on the wire. CPAL helpfully
+    /// gives us an estimate. The latest estimate of latency is converted to a duration in terms
+    /// of number of samples and provided as `output_latency`. Added to this is the current
+    /// number of samples we expect remain to be processed from the last read.
+    ///
+    /// Call this whenever bytes have been read out of the buffer.
+    fn update_output_buffer(
+        &mut self,
+        samples_to_play: usize,
+        capacity: usize,
+        output_latency: usize,
+    );
+
+    /// Supply the next frame available from the TNC, if it was requested.
+    fn provide_next_frame(&mut self, frame: Option<ModulatorFrame>);
+
+    /// Calculate and write out output samples for the soundcard.
+    ///
+    /// Returns the number of bytes valid in `out`. Should generally be called in a loop until
+    /// 0 is returned.
+    fn read_output_samples(&mut self, out: &mut [i16]) -> usize;
+
+    /// Run the modulator and receive actions to process.
+    ///
+    /// Should be called in a loop until it returns `None`.
+    fn run(&mut self) -> Option<ModulatorAction>;
+}
+
+pub enum ModulatorAction {
+    /// If true, once all samples have been exhausted output should revert to equilibrium.
+    ///
+    /// If false, failure to pick up enough samples for output sound card is an underrun error.
+    SetIdle(bool),
+
+    /// Check with the TNC if there is a frame available for transmission.
+    ///
+    /// Call `next_frame()` with either the next frame, or `None` if TNC has nothing more to offer.
+    GetNextFrame,
+
+    /// Modulator wishes to send samples to the output buffer - call `read_output_samples`.
+    ReadOutput,
+
+    /// Advise the TNC that we will complete sending End Of Transmission after the given number of
+    /// samples has elapsed, and therefore PTT should be deasserted at this time.
+    TransmissionWillEnd(usize),
+}
+
+/// Frames for transmission, emitted by the TNC and received by the Modulator.
+///
+/// The TNC is responsible for all timing decisions, making sure these frames are emitted in the
+/// correct order, breaks between transmissions, PTT and CSMA. If the modulator is given a
+/// `ModulatorFrame` value, its job is to transmit it immediately by modulating it into the output
+/// buffer, or otherwise directly after any previously-supplied frames.
+///
+/// The modulator controls the rate at which frames are drawn out of the TNC. Therefore if the send
+/// rate is too high (or there is too much channel activity) then the effect of this backpressure is
+/// that the TNC's internal queues will overflow and it will either discard earlier frames in the
+/// current stream, or some packets awaiting transmission.
+pub enum ModulatorFrame {
+    Preamble {
+        /// TNC's configured TxDelay setting, increments of 10ms.
+        ///
+        /// TNC fires PTT and it's up to modulator to apply the setting, taking advantage of whatever
+        /// buffering already exists in the sound card to reduce the artificial delay.
+        tx_delay: u8,
+    },
+    Lsf(LsfFrame),
+    Stream(StreamFrame),
+    Packet(PacketFrame),
+    // TODO: BertFrame
+    EndOfTransmission,
+}
+
+pub struct SoftModulator {
+    // TODO: 2000 was overflowing around EOT, track down why
+    /// Next modulated frame to output - 1920 samples for 40ms frame plus 80 for ramp-down
+    next_transmission: [i16; 4000],
+    /// How much of next_transmission should in fact be transmitted
+    next_len: usize,
+    /// How much of next_transmission has been read out
+    next_read: usize,
+    /// How many pending zero samples to emit to align start of preamble with PTT taking effect
+    tx_delay_padding: usize,
+
+    /// Do we need to update idle state?
+    update_idle: bool,
+    /// What is that idle status?
+    idle: bool,
+
+    /// Do we need to calculate a transmission end time?
+    ///
+    /// (True after we encoded an EOT.) We will wait until we get a precise timing update.
+    calculate_tx_end: bool,
+    /// Do we need to report a transmission end time?
+    ///
+    /// This is a duration expressed in number of samples.
+    report_tx_end: Option<usize>,
+
+    /// Circular buffer of most recently output samples for calculating the RRC filtered value.
+    ///
+    /// This should naturally degrade to an oldest value plus 80 zeroes after an EOT.
+    filter_win: [f32; 81],
+    /// Current position in filter_win
+    filter_cursor: usize,
+
+    /// Should we ask the TNC for another frame. True after each call to update_output_buffer.
+    try_get_frame: bool,
+
+    /// Expected delay beyond the buffer to reach the DAC
+    output_latency: usize,
+    /// Number of samples we have placed in the buffer for the output soundcard not yet picked up.
+    samples_in_buf: usize,
+    /// Total size to which the output buffer is allowed to expand.
+    buf_capacity: usize,
+}
+
+impl SoftModulator {
+    pub fn new() -> Self {
+        Self {
+            next_transmission: [0i16; 4000],
+            next_len: 0,
+            next_read: 0,
+            tx_delay_padding: 0,
+            update_idle: true,
+            idle: true,
+            calculate_tx_end: false,
+            report_tx_end: None,
+            filter_win: [0f32; 81],
+            filter_cursor: 0,
+            try_get_frame: false,
+            output_latency: 0,
+            samples_in_buf: 0,
+            buf_capacity: 0,
+        }
+    }
+
+    fn push_sample(&mut self, dibit: f32) {
+        // TODO: 48 kHz assumption again
+        for i in 0..10 {
+            // Right now we are encoding everything as 1.0-scaled dibit floats
+            // This is a bit silly but it will do for a minute
+            // Max possible gain from the RRC filter with upsampling is about 0.462
+            // Let's bump everything to a baseline of 16383 / 0.462 = 35461
+            // For normal signals this yields roughly 0.5 magnitude which is plenty
+            if i == 0 {
+                self.filter_win[self.filter_cursor] = dibit * 35461.0;
+            } else {
+                self.filter_win[self.filter_cursor] = 0.0;
+            }
+            self.filter_cursor = (self.filter_cursor + 1) % 81;
+            let mut out: f32 = 0.0;
+            for i in 0..81 {
+                let filter_idx = (self.filter_cursor + i) % 81;
+                out += RRC_48K[i] * self.filter_win[filter_idx];
+            }
+            self.next_transmission[self.next_len] = out as i16;
+            self.next_len += 1;
+        }
+    }
+
+    fn request_frame_if_space(&mut self) {
+        if self.buf_capacity - self.samples_in_buf >= 2000 {
+            self.try_get_frame = true;
+        }
+    }
+}
+
+impl Modulator for SoftModulator {
+    fn update_output_buffer(
+        &mut self,
+        samples_to_play: usize,
+        capacity: usize,
+        output_latency: usize,
+    ) {
+        //log::debug!("modulator update_output_buffer {samples_to_play} {capacity} {output_latency}");
+        self.output_latency = output_latency;
+        self.buf_capacity = capacity;
+        self.samples_in_buf = samples_to_play;
+
+        if self.calculate_tx_end {
+            self.calculate_tx_end = false;
+            // next_transmission should already have been read out to the buffer by now
+            // so we don't have to consider it
+            self.report_tx_end = Some(self.samples_in_buf + self.output_latency);
+        }
+
+        self.request_frame_if_space();
+    }
+
+    fn provide_next_frame(&mut self, frame: Option<ModulatorFrame>) {
+        let Some(frame) = frame else {
+            self.try_get_frame = false;
+            return;
+        };
+
+        self.next_len = 0;
+        self.next_read = 0;
+
+        match frame {
+            ModulatorFrame::Preamble { tx_delay } => {
+                // TODO: Stop assuming 48 kHz everywhere. 24 kHz should be fine too.
+                let tx_delay_samples = tx_delay as usize * 480;
+                // TxDelay and output latency have the same effect - account for whichever is bigger.
+                // We want our sound card DAC hitting preamble right when PTT fully engages.
+                // The modulator calls the shots here - TNC hands over Preamble and asserts PTT, then
+                // waits to be told when transmission will be complete. This estimate will not be
+                // made and delivered until we generate the EOT frame.
+                self.tx_delay_padding = tx_delay_samples.max(self.output_latency);
+
+                // We should be starting from a filter_win of zeroes
+                // Transmission is effectively smeared by 80 taps and we'll capture that in EOT
+                for dibit in generate_preamble() {
+                    self.push_sample(dibit);
+                }
+            }
+            ModulatorFrame::Lsf(lsf_frame) => {
+                for dibit in encode_lsf(&lsf_frame) {
+                    self.push_sample(dibit);
+                }
+            }
+            ModulatorFrame::Stream(stream_frame) => {
+                for dibit in encode_stream(&stream_frame) {
+                    self.push_sample(dibit);
+                }
+            }
+            ModulatorFrame::Packet(packet_frame) => {
+                for dibit in encode_packet(&packet_frame) {
+                    self.push_sample(dibit);
+                }
+            }
+            ModulatorFrame::EndOfTransmission => {
+                for dibit in generate_end_of_transmission() {
+                    self.push_sample(dibit);
+                }
+                for _ in 0..80 {
+                    // This is not a real symbol value
+                    // However we want to flush the filter
+                    self.push_sample(0f32);
+                }
+                self.calculate_tx_end = true;
+            }
+        }
+    }
+
+    fn read_output_samples(&mut self, out: &mut [i16]) -> usize {
+        let mut written = 0;
+
+        // if we have pre-TX padding to accommodate TxDelay then expend that first
+        if self.tx_delay_padding > 0 {
+            let len = out.len().min(self.tx_delay_padding);
+            self.tx_delay_padding -= len;
+            for x in 0..len {
+                out[x] = 0;
+            }
+            written += len;
+        }
+
+        // then follow it with whatever might be left in next_transmission
+        let next_remaining = self.next_len - self.next_read;
+        if next_remaining > 0 {
+            let len = (out.len() - written).min(next_remaining);
+            out[written..(written + len)]
+                .copy_from_slice(&self.next_transmission[self.next_read..(self.next_read + len)]);
+            self.next_read += len;
+            written += len;
+        }
+
+        written
+    }
+
+    fn run(&mut self) -> Option<ModulatorAction> {
+        // Time-sensitive for accuracy, so handle it first
+        if let Some(end) = self.report_tx_end.take() {
+            return Some(ModulatorAction::TransmissionWillEnd(end));
+        }
+
+        if self.next_read < self.next_len {
+            return Some(ModulatorAction::ReadOutput);
+        }
+
+        if self.update_idle {
+            self.update_idle = false;
+            return Some(ModulatorAction::SetIdle(self.idle));
+        }
+
+        if self.try_get_frame {
+            return Some(ModulatorAction::GetNextFrame);
+        }
+
+        None
+    }
+}
+
+impl Default for SoftModulator {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 #[derive(Debug)]
 pub(crate) struct DecodeCandidate {
     burst: SyncBurst,
 #[derive(Debug)]
 pub(crate) struct DecodeCandidate {
     burst: SyncBurst,