Skip to main content

sdr_iq_file_reader/
lib.rs

1//! # SDR IQ File Reader
2//!
3//! This library provides a simple way to read samples from an SDR IQ file through the [`SdrFileReader`] struct.
4//! See the [`SdrFileReader`] documentation for more information on how to use it.
5
6use bon::bon;
7use num_complex::Complex;
8use std::fs::File;
9use std::io::{BufReader, ErrorKind, Read};
10use std::path::Path;
11
12/// Create a new `SdrFileReader` using the builder pattern.
13/// Then call `read_next_chunk_complexf32` or `read_next_chunk_complexf64` to read the samples.
14///
15/// # Example
16/// ```
17/// use sdr_iq_file_reader::{SdrFileReader, SampleType};
18/// let mut reader = SdrFileReader::builder()
19///     .file_path("gqrx_20240929_015218_580206500_2400000_fc.raw")
20///     .samples_per_chunk(1024)
21///     .sample_type(SampleType::F32)
22///     .build()
23///     .expect("Failed to create SdrFileReader");
24/// let samples = reader.read_next_chunk_complexf32().unwrap();
25/// ```
26pub struct SdrFileReader {
27    reader: BufReader<File>,
28    samples_per_chunk: usize,
29    sample_type: SampleType,
30}
31
32/// The type of samples in the SDR file
33/// You will have to look up what your SDR/software uses.
34pub enum SampleType {
35    /// Samples stored as unsigned 8-bit integers
36    U8,
37    /// Samples stored as signed 8-bit integers
38    I8,
39    /// Samples stored as unsigned 16-bit integers
40    U16,
41    /// Samples stored as signed 16-bit integers
42    I16,
43    /// Samples stored as 32-bit floating point numbers
44    F32,
45    /// Samples stored as 64-bit floating point numbers
46    F64,
47}
48
49impl SampleType {
50    /// The number of bytes per sample.
51    /// One sample has 2 values: I and Q.
52    /// Therefore the total number of bytes per sample twice the length of the datatype.
53    #[must_use]
54    pub fn sample_len(&self) -> usize {
55        match self {
56            SampleType::U8 | SampleType::I8 => 2,
57            SampleType::I16 | SampleType::U16 => 4,
58            SampleType::F32 => 8,
59            SampleType::F64 => 16,
60        }
61    }
62}
63
64#[bon]
65impl SdrFileReader {
66    #[allow(missing_docs)]
67    #[builder]
68    pub fn new(
69        file_path: impl AsRef<Path>,
70        samples_per_chunk: usize,
71        sample_type: SampleType,
72    ) -> Result<Self, std::io::Error> {
73        let file = File::open(file_path)?;
74        let reader = BufReader::new(file);
75        Ok(SdrFileReader {
76            reader,
77            samples_per_chunk,
78            sample_type,
79        })
80    }
81
82    /// Read the next chunk of samples as Complex<f32> from the file.
83    ///
84    /// # Warning
85    /// If you have set the sample type to `SampleType::F64`, you should use `read_next_chunk_complexf64` instead. Otherwise, the values will be truncated and you will lose accuracy.
86    ///
87    /// # Returns
88    /// - `Ok(Some(samples))` if there are samples in the chunk
89    /// - `Ok(None)` if the end of the file is reached
90    ///
91    /// # Errors
92    /// - `std::io::Error` if there was an error reading the file other than reaching the end
93    pub fn read_next_chunk_complexf32(
94        &mut self,
95    ) -> Result<Option<Vec<Complex<f32>>>, std::io::Error> {
96        let mut buffer = vec![0u8; self.samples_per_chunk * self.sample_type.sample_len()]; // 2 for I and Q
97        match self.reader.read_exact(&mut buffer) {
98            Ok(()) => {
99                let mut samples = Vec::with_capacity(self.samples_per_chunk);
100                match self.sample_type {
101                    SampleType::U8 => buffer
102                        .chunks_exact(self.sample_type.sample_len())
103                        .for_each(|s| samples.push(Complex::new(f32::from(s[0]), f32::from(s[1])))),
104                    SampleType::I8 => {
105                        buffer
106                            .chunks_exact(self.sample_type.sample_len())
107                            .for_each(|s| {
108                                samples.push(Complex::new(
109                                    f32::from(i8::from_ne_bytes([s[0]])),
110                                    f32::from(i8::from_ne_bytes([s[1]])),
111                                ));
112                            });
113                    }
114                    SampleType::U16 => {
115                        buffer
116                            .chunks_exact(self.sample_type.sample_len())
117                            .for_each(|s| {
118                                samples.push(Complex::new(
119                                    f32::from(u16::from_ne_bytes([s[0], s[1]])),
120                                    f32::from(u16::from_ne_bytes([s[2], s[3]])),
121                                ));
122                            });
123                    }
124                    SampleType::I16 => {
125                        buffer
126                            .chunks_exact(self.sample_type.sample_len())
127                            .for_each(|s| {
128                                samples.push(Complex::new(
129                                    f32::from(i16::from_ne_bytes([s[0], s[1]])),
130                                    f32::from(i16::from_ne_bytes([s[2], s[3]])),
131                                ));
132                            });
133                    }
134                    SampleType::F32 => {
135                        buffer
136                            .chunks_exact(self.sample_type.sample_len())
137                            .for_each(|s| {
138                                samples.push(Complex::new(
139                                    f32::from_ne_bytes([s[0], s[1], s[2], s[3]]),
140                                    f32::from_ne_bytes([s[4], s[5], s[6], s[7]]),
141                                ));
142                            });
143                    }
144                    #[allow(clippy::cast_possible_truncation)]
145                    SampleType::F64 => {
146                        buffer
147                            .chunks_exact(self.sample_type.sample_len())
148                            .for_each(|s| {
149                                samples.push(Complex::new(
150                                    f64::from_ne_bytes([
151                                        s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
152                                    ]) as f32,
153                                    f64::from_ne_bytes([
154                                        s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15],
155                                    ]) as f32,
156                                ));
157                            });
158                    }
159                }
160                Ok(Some(samples))
161            }
162            Err(why) => match why.kind() {
163                ErrorKind::UnexpectedEof => Ok(None),
164                _ => Err(why),
165            },
166        }
167    }
168
169    /// Read the next chunk of samples as Complex<f64> from the file.
170    ///
171    /// # Returns
172    /// - `Ok(Some(samples))` if there are samples in the chunk
173    /// - `Ok(None)` if the end of the file is reached
174    ///
175    /// # Errors
176    /// - `std::io::Error` if there was an error reading the file other than reaching the end
177    pub fn read_next_chunk_complexf64(
178        &mut self,
179    ) -> Result<Option<Vec<Complex<f64>>>, std::io::Error> {
180        let mut buffer = vec![0u8; self.samples_per_chunk * self.sample_type.sample_len()]; // 2 for I and Q
181        match self.reader.read_exact(&mut buffer) {
182            Ok(()) => {
183                let mut samples = Vec::with_capacity(self.samples_per_chunk);
184                match self.sample_type {
185                    SampleType::U8 => buffer
186                        .chunks_exact(self.sample_type.sample_len())
187                        .for_each(|s| samples.push(Complex::new(f64::from(s[0]), f64::from(s[1])))),
188                    SampleType::I8 => {
189                        buffer
190                            .chunks_exact(self.sample_type.sample_len())
191                            .for_each(|s| {
192                                samples.push(Complex::new(
193                                    f64::from(i8::from_ne_bytes([s[0]])),
194                                    f64::from(i8::from_ne_bytes([s[1]])),
195                                ));
196                            });
197                    }
198                    SampleType::U16 => {
199                        buffer
200                            .chunks_exact(self.sample_type.sample_len())
201                            .for_each(|s| {
202                                samples.push(Complex::new(
203                                    f64::from(u16::from_ne_bytes([s[0], s[1]])),
204                                    f64::from(u16::from_ne_bytes([s[2], s[3]])),
205                                ));
206                            });
207                    }
208                    SampleType::I16 => {
209                        buffer
210                            .chunks_exact(self.sample_type.sample_len())
211                            .for_each(|s| {
212                                samples.push(Complex::new(
213                                    f64::from(i16::from_ne_bytes([s[0], s[1]])),
214                                    f64::from(i16::from_ne_bytes([s[2], s[3]])),
215                                ));
216                            });
217                    }
218                    SampleType::F32 => {
219                        buffer
220                            .chunks_exact(self.sample_type.sample_len())
221                            .for_each(|s| {
222                                samples.push(Complex::new(
223                                    f64::from(f32::from_ne_bytes([s[0], s[1], s[2], s[3]])),
224                                    f64::from(f32::from_ne_bytes([s[4], s[5], s[6], s[7]])),
225                                ));
226                            });
227                    }
228                    SampleType::F64 => {
229                        buffer
230                            .chunks_exact(self.sample_type.sample_len())
231                            .for_each(|s| {
232                                samples.push(Complex::new(
233                                    f64::from_ne_bytes([
234                                        s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
235                                    ]),
236                                    f64::from_ne_bytes([
237                                        s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15],
238                                    ]),
239                                ));
240                            });
241                    }
242                }
243                Ok(Some(samples))
244            }
245            Err(why) => match why.kind() {
246                ErrorKind::UnexpectedEof => Ok(None),
247                _ => Err(why),
248            },
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_sdr_file_reader_f32() {
259        let file_path = "gqrx_20240929_015218_580206500_2400000_fc.raw";
260        let mut reader = SdrFileReader::builder()
261            .file_path(file_path)
262            .samples_per_chunk(1024)
263            .sample_type(SampleType::F32)
264            .build()
265            .expect("Failed to create SdrFileReader");
266        while let Some(samples) = reader.read_next_chunk_complexf32().unwrap() {
267            assert_eq!(samples.len(), 1024);
268            // Further assertions can be added based on expected values
269        }
270    }
271
272    #[test]
273    fn test_sdr_file_reader_f64() {
274        let file_path = "gqrx_20240929_015218_580206500_2400000_fc.raw";
275        let mut reader = SdrFileReader::builder()
276            .file_path(file_path)
277            .samples_per_chunk(1024)
278            .sample_type(SampleType::F32)
279            .build()
280            .expect("Failed to create SdrFileReader");
281        while let Some(samples) = reader.read_next_chunk_complexf64().unwrap() {
282            assert_eq!(samples.len(), 1024);
283            // Further assertions can be added based on expected values
284        }
285    }
286
287    #[test]
288    fn test_compare_f32_f64_readings() {
289        let file_path = "gqrx_20240929_015218_580206500_2400000_fc.raw";
290        let mut reader_f32 = SdrFileReader::builder()
291            .file_path(file_path)
292            .samples_per_chunk(1024)
293            .sample_type(SampleType::F32)
294            .build()
295            .expect("Failed to create SdrFileReader for f32");
296
297        let mut reader_f64 = SdrFileReader::builder()
298            .file_path(file_path)
299            .samples_per_chunk(1024)
300            .sample_type(SampleType::F32)
301            .build()
302            .expect("Failed to create SdrFileReader for f64");
303
304        while let (Some(samples_f32), Some(samples_f64)) = (
305            reader_f32.read_next_chunk_complexf32().unwrap(),
306            reader_f64.read_next_chunk_complexf64().unwrap(),
307        ) {
308            assert_eq!(samples_f32.len(), samples_f64.len());
309            for (s32, s64) in samples_f32.iter().zip(samples_f64.iter()) {
310                // Compare with epsilon
311                assert!((f64::from(s32.re) - s64.re).abs() < f64::from(f32::EPSILON));
312                assert!((f64::from(s32.im) - s64.im).abs() < f64::from(f32::EPSILON));
313            }
314        }
315    }
316}