]> code.octet-stream.net Git - m17rt/blobdiff - m17core/src/modem.rs
Successful round tripping wav -> rrc -> audio out
[m17rt] / m17core / src / modem.rs
index fa06af6c8f4bfe24855d46096784408871d72156..b2ab77312f62806a83150038469bbbd9c06f1d4f 100644 (file)
@@ -1,6 +1,9 @@
 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;
@@ -169,25 +172,19 @@ pub trait Modulator {
     /// 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`.
-    ///
-    /// Finally, we give the modem a monotonic timer expressed in the number of samples of time
-    /// that have elapsed since the modem began operation. When the modulator advises the TNC
-    /// exactly when a transmission is expected to complete, it should be expressed in terms of
-    /// this number.
+    /// 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, or the TNC may have something
-    /// new to send.
+    /// 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,
-        now_samples: u64,
     );
 
     /// Supply the next frame available from the TNC, if it was requested.
-    fn next_frame(&mut self, frame: Option<ModulatorFrame>);
+    fn provide_next_frame(&mut self, frame: Option<ModulatorFrame>);
 
     /// Calculate and write out output samples for the soundcard.
     ///
@@ -215,8 +212,9 @@ pub enum ModulatorAction {
     /// 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 at the given time and
-    TransmissionWillEnd(u64),
+    /// 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.
@@ -245,9 +243,10 @@ pub enum ModulatorFrame {
     EndOfTransmission,
 }
 
-struct SoftModulator {
-    /// Next modulated frame to output - 1920 samples for 40ms frame plus 80 for ramp-up/ramp-down
-    next_transmission: [i16; 2000],
+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
@@ -260,55 +259,160 @@ struct SoftModulator {
     /// What is that idle status?
     idle: bool,
 
-    /// Do we need to report that a transmission end time? (True after we encoded an EOT)
-    report_tx_end: bool,
-    /// What will that end time be?
-    tx_end_time: u64,
+    /// 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 incoming samples for calculating the RRC filtered value
-    filter_win: [i16; 81],
+    /// 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,
 
-    now: u64,
+    /// 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 theoretical gain from the RRC filter is 4.328
+            // Let's bump everything to a baseline of 16383 / 4.328 = 3785.35
+            // This is not particularly high but at least we won't ever hit the top
+            if i == 0 {
+                // 10x the impulse with zeroes between for upsampling
+                self.filter_win[self.filter_cursor] = dibit * 3785.0 * 10.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,
-        now_samples: u64,
     ) {
-        self.now = now_samples;
+        //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 capacity - samples_to_play >= 2000 {
-            self.try_get_frame = true;
+        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 next_frame(&mut self, frame: Option<ModulatorFrame>) {
+    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 {
-            Some(_f) => {}
-            None => {
-                self.try_get_frame = false;
+            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;
             }
         }
-
-        // this is where we write it to next_transmission
-
-        // this is where we set report_tx_end
-        todo!()
     }
 
     fn read_output_samples(&mut self, out: &mut [i16]) -> usize {
@@ -327,7 +431,7 @@ impl Modulator for SoftModulator {
         // 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).max(next_remaining);
+            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;
@@ -338,6 +442,11 @@ impl Modulator for SoftModulator {
     }
 
     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);
         }
@@ -347,11 +456,6 @@ impl Modulator for SoftModulator {
             return Some(ModulatorAction::SetIdle(self.idle));
         }
 
-        if self.report_tx_end {
-            self.report_tx_end = false;
-            return Some(ModulatorAction::TransmissionWillEnd(self.tx_end_time));
-        }
-
         if self.try_get_frame {
             return Some(ModulatorAction::GetNextFrame);
         }