tried to do multithreading, but that didn't work, so just did an overhaul of the demod and made it a bit better instead
This commit is contained in:
parent
806fb76829
commit
5694ce2f55
|
@ -1,16 +1,88 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lapp"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "threnodyne"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lapp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
"checksum crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
|
||||
"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
|
||||
"checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
|
||||
"checksum lapp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60bf485afeba9437a275ad29a9383b03f2978450e7feceffb55be8c0dbad9829"
|
||||
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
"checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
|
||||
"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
"checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
|
||||
|
|
|
@ -8,3 +8,5 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
lapp = "0.4.0"
|
||||
crossbeam-channel = "0.4.2"
|
||||
num_cpus = "1.12.0"
|
||||
|
|
|
@ -63,3 +63,8 @@ channel with heavy interference and tight bandwidth limits. Data transfer rate w
|
|||
you'll be lucky to get 300 bits/sec over the air, and luckier still to get 100 bits/sec over a
|
||||
tight analog voice link - but this is still reasonable enough for plaintext messages.
|
||||
|
||||
### Origin of name
|
||||
A threnody is a song of mourning, similar to a lament. In earlier versions of this project, we were
|
||||
expecting the sound it produced to be more musical, perhaps with the warble characteristic of PSK,
|
||||
but then we settled on chirp spread spectrum instead, and now it sounds like a cicada. It is what it
|
||||
is, we guess. It's a pretty name and we don't feel like changing it now.
|
||||
|
|
|
@ -2,313 +2,525 @@
|
|||
|
||||
use crate::posi;
|
||||
use crate::chirp;
|
||||
use crate::gauss;
|
||||
use crate::gaussian;
|
||||
|
||||
struct SignalInput
|
||||
{
|
||||
template_rise: Vec<i64>,
|
||||
template_fall: Vec<i64>,
|
||||
norm_rise: i64,
|
||||
norm_fall: i64,
|
||||
symbol_size: i64,
|
||||
buffer: posi::Buffer<i64>,
|
||||
sum: i64,
|
||||
}
|
||||
|
||||
impl SignalInput
|
||||
{
|
||||
fn new
|
||||
(
|
||||
center_freq: f32,
|
||||
deviation: f32,
|
||||
symbol_length: f32,
|
||||
) ->
|
||||
SignalInput
|
||||
{
|
||||
let upper_freq = (center_freq + deviation).max(0.0).min(1.0);
|
||||
let lower_freq = (center_freq - deviation).max(0.0).min(1.0);
|
||||
|
||||
// these have to be reversed, since the input buffer shifts in at index 0 and is thus a mirror image
|
||||
let template_rise:Vec<i64> =
|
||||
(
|
||||
chirp::generate(upper_freq, lower_freq, symbol_length)
|
||||
.into_iter()
|
||||
.map(|x| x as i64)
|
||||
.collect()
|
||||
);
|
||||
let template_fall:Vec<i64> =
|
||||
(
|
||||
chirp::generate(lower_freq, upper_freq, symbol_length)
|
||||
.into_iter()
|
||||
.map(|x| x as i64)
|
||||
.collect()
|
||||
);
|
||||
|
||||
let norm_rise =
|
||||
(
|
||||
template_rise
|
||||
.iter()
|
||||
.map(|&x| x.abs())
|
||||
.sum()
|
||||
);
|
||||
let norm_fall =
|
||||
(
|
||||
template_rise
|
||||
.iter()
|
||||
.map(|&x| x.abs())
|
||||
.sum()
|
||||
);
|
||||
|
||||
SignalInput
|
||||
{
|
||||
template_rise: template_rise,
|
||||
template_fall: template_fall,
|
||||
norm_rise: norm_rise,
|
||||
norm_fall: norm_fall,
|
||||
symbol_size: symbol_length.round() as i64,
|
||||
buffer: posi::Buffer::new(symbol_length.round() as usize),
|
||||
sum: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest
|
||||
(
|
||||
&mut self,
|
||||
sample: i16,
|
||||
) {
|
||||
let new_value = sample as i64;
|
||||
let old_value = self.buffer.last();
|
||||
|
||||
self.sum -= old_value.abs();
|
||||
self.sum += new_value.abs();
|
||||
|
||||
self.buffer.shift_in(new_value);
|
||||
}
|
||||
|
||||
fn process
|
||||
(
|
||||
&self,
|
||||
) ->
|
||||
(i64, i64)
|
||||
{
|
||||
let local_signal = self.buffer.to_vec();
|
||||
|
||||
let mut rise_value = 0;
|
||||
let mut fall_value = 0;
|
||||
|
||||
if self.sum > 0
|
||||
{
|
||||
for (&a, &b) in
|
||||
(
|
||||
local_signal.iter()
|
||||
).zip(
|
||||
self.template_rise.iter()
|
||||
) {
|
||||
rise_value += a * b;
|
||||
}
|
||||
|
||||
for (&a, &b) in
|
||||
(
|
||||
local_signal.iter()
|
||||
).zip(
|
||||
self.template_fall.iter()
|
||||
) {
|
||||
fall_value += a * b;
|
||||
}
|
||||
|
||||
rise_value =
|
||||
(
|
||||
(32768 * rise_value * self.symbol_size)
|
||||
/
|
||||
(self.norm_rise * self.sum)
|
||||
);
|
||||
|
||||
fall_value =
|
||||
(
|
||||
(32768 * fall_value * self.symbol_size)
|
||||
/
|
||||
(self.norm_fall * self.sum)
|
||||
);
|
||||
}
|
||||
|
||||
return (rise_value, fall_value);
|
||||
}
|
||||
}
|
||||
|
||||
struct CorrelFilter
|
||||
{
|
||||
kernel: Vec<i64>,
|
||||
buffer_rise: posi::Buffer<i64>,
|
||||
buffer_fall: posi::Buffer<i64>,
|
||||
}
|
||||
|
||||
impl CorrelFilter
|
||||
{
|
||||
fn new
|
||||
(
|
||||
freq: f32,
|
||||
) ->
|
||||
CorrelFilter
|
||||
{
|
||||
let kernel = gaussian::generate(freq);
|
||||
let size = kernel.len();
|
||||
|
||||
CorrelFilter
|
||||
{
|
||||
kernel: kernel,
|
||||
buffer_rise: posi::Buffer::new(size),
|
||||
buffer_fall: posi::Buffer::new(size),
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest
|
||||
(
|
||||
&mut self,
|
||||
rise: i64,
|
||||
fall: i64,
|
||||
) {
|
||||
self.buffer_rise.shift_in(rise);
|
||||
self.buffer_fall.shift_in(fall);
|
||||
}
|
||||
|
||||
fn hi_process
|
||||
(
|
||||
&self,
|
||||
) ->
|
||||
(i64, i64)
|
||||
{
|
||||
let local_rise = self.buffer_rise.to_vec();
|
||||
let local_fall = self.buffer_fall.to_vec();
|
||||
|
||||
let mut rise_result:i64 = 0;
|
||||
let mut fall_result:i64 = 0;
|
||||
|
||||
for (&a, &b) in
|
||||
(
|
||||
local_rise.iter()
|
||||
).zip(
|
||||
self.kernel.iter()
|
||||
) {
|
||||
rise_result += a * b;
|
||||
}
|
||||
|
||||
for (&a, &b) in
|
||||
(
|
||||
local_fall.iter()
|
||||
).zip(
|
||||
self.kernel.iter()
|
||||
) {
|
||||
fall_result += a * b;
|
||||
}
|
||||
|
||||
rise_result /= 32768;
|
||||
fall_result /= 32768;
|
||||
|
||||
return
|
||||
(
|
||||
self.buffer_rise.mid() - rise_result,
|
||||
self.buffer_fall.mid() - fall_result,
|
||||
);
|
||||
}
|
||||
|
||||
fn lo_process
|
||||
(
|
||||
&self,
|
||||
) ->
|
||||
(i64, i64)
|
||||
{
|
||||
let local_rise = self.buffer_rise.to_vec();
|
||||
let local_fall = self.buffer_fall.to_vec();
|
||||
|
||||
let mut rise_result:i64 = 0;
|
||||
let mut fall_result:i64 = 0;
|
||||
|
||||
for (&a, &b) in
|
||||
(
|
||||
local_rise.iter()
|
||||
).zip(
|
||||
self.kernel.iter()
|
||||
) {
|
||||
rise_result += a * b;
|
||||
}
|
||||
|
||||
for (&a, &b) in
|
||||
(
|
||||
local_fall.iter()
|
||||
).zip(
|
||||
self.kernel.iter()
|
||||
) {
|
||||
fall_result += a * b;
|
||||
}
|
||||
|
||||
rise_result /= 32768;
|
||||
fall_result /= 32768;
|
||||
|
||||
return
|
||||
(
|
||||
rise_result,
|
||||
fall_result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct CorrelOutput
|
||||
{
|
||||
buffer_rise: posi::Buffer<i64>,
|
||||
buffer_fall: posi::Buffer<i64>,
|
||||
rise_maxima: Vec<i64>,
|
||||
fall_maxima: Vec<i64>,
|
||||
rolling_max_rise: i64,
|
||||
rolling_max_fall: i64,
|
||||
rolling_max_decay: i64,
|
||||
squelch: i64,
|
||||
}
|
||||
|
||||
impl CorrelOutput
|
||||
{
|
||||
fn new
|
||||
(
|
||||
size: usize,
|
||||
) ->
|
||||
CorrelOutput
|
||||
{
|
||||
CorrelOutput
|
||||
{
|
||||
buffer_rise: posi::Buffer::new(size),
|
||||
buffer_fall: posi::Buffer::new(size),
|
||||
rise_maxima: Vec::new(),
|
||||
fall_maxima: Vec::new(),
|
||||
rolling_max_rise: 0,
|
||||
rolling_max_fall: 0,
|
||||
rolling_max_decay: (size as i64) / 2,
|
||||
squelch: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest
|
||||
(
|
||||
&mut self,
|
||||
new_rise: i64,
|
||||
new_fall: i64,
|
||||
) {
|
||||
let old_rise = self.buffer_rise.last();
|
||||
let old_fall = self.buffer_fall.last();
|
||||
|
||||
if let Ok(index) = self.rise_maxima.binary_search(&old_rise)
|
||||
{
|
||||
self.rise_maxima.remove(index);
|
||||
}
|
||||
if let Ok(index) = self.fall_maxima.binary_search(&old_fall)
|
||||
{
|
||||
self.fall_maxima.remove(index);
|
||||
}
|
||||
|
||||
if (
|
||||
self.rise_maxima.len() == 0
|
||||
||
|
||||
Some(&new_rise) >= self.rise_maxima.last()
|
||||
) {
|
||||
self.rise_maxima.push(new_rise);
|
||||
}
|
||||
if (
|
||||
self.fall_maxima.len() == 0
|
||||
||
|
||||
Some(&new_fall) >= self.fall_maxima.last()
|
||||
) {
|
||||
self.fall_maxima.push(new_fall);
|
||||
}
|
||||
|
||||
self.buffer_rise.shift_in(new_rise);
|
||||
self.buffer_fall.shift_in(new_fall);
|
||||
|
||||
let current_rise = self.buffer_rise.mid();
|
||||
let current_fall = self.buffer_fall.mid();
|
||||
|
||||
self.rolling_max_rise =
|
||||
(
|
||||
self.rolling_max_rise
|
||||
*
|
||||
(self.rolling_max_decay - 1)
|
||||
/
|
||||
self.rolling_max_decay
|
||||
);
|
||||
|
||||
self.rolling_max_fall =
|
||||
(
|
||||
self.rolling_max_fall
|
||||
*
|
||||
(self.rolling_max_decay - 1)
|
||||
/
|
||||
self.rolling_max_decay
|
||||
);
|
||||
|
||||
self.rolling_max_rise = self.rolling_max_rise.max(current_rise);
|
||||
self.rolling_max_fall = self.rolling_max_fall.max(current_fall);
|
||||
}
|
||||
|
||||
fn have_peaks
|
||||
(
|
||||
&self,
|
||||
) ->
|
||||
(bool, bool)
|
||||
{
|
||||
let current_rise = self.buffer_rise.mid();
|
||||
let current_fall = self.buffer_fall.mid();
|
||||
|
||||
let rise_max = *self.rise_maxima.last().unwrap_or(&0);
|
||||
let fall_max = *self.fall_maxima.last().unwrap_or(&0);
|
||||
|
||||
let mut have_rise:bool = false;
|
||||
let mut have_fall:bool = false;
|
||||
|
||||
if (
|
||||
current_rise >= rise_max
|
||||
&&
|
||||
current_rise >= self.rolling_max_rise * 15 / 16
|
||||
&&
|
||||
current_rise >= self.squelch
|
||||
) {
|
||||
have_rise = true;
|
||||
}
|
||||
if (
|
||||
current_fall >= fall_max
|
||||
&&
|
||||
current_fall >= self.rolling_max_fall * 15 / 16
|
||||
&&
|
||||
current_fall >= self.squelch
|
||||
) {
|
||||
have_fall = true;
|
||||
}
|
||||
|
||||
return (have_rise, have_fall);
|
||||
}
|
||||
|
||||
fn have_bit
|
||||
(
|
||||
&self
|
||||
) ->
|
||||
Option<bool>
|
||||
{
|
||||
match self.have_peaks()
|
||||
{
|
||||
(true, false) => Some(true),
|
||||
(false, true) => Some(false),
|
||||
( _, _ ) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ByteAccumulator
|
||||
{
|
||||
byte: u8,
|
||||
bit: u8,
|
||||
since_bit: usize,
|
||||
abandon: usize,
|
||||
}
|
||||
|
||||
impl ByteAccumulator
|
||||
{
|
||||
fn new
|
||||
(
|
||||
abandon: usize,
|
||||
) ->
|
||||
ByteAccumulator
|
||||
{
|
||||
ByteAccumulator
|
||||
{
|
||||
byte: 0x00,
|
||||
bit: 0,
|
||||
since_bit: 0,
|
||||
abandon: abandon,
|
||||
}
|
||||
}
|
||||
|
||||
fn accumulate
|
||||
(
|
||||
&mut self,
|
||||
possible_bit: Option<bool>,
|
||||
) ->
|
||||
Option<u8>
|
||||
{
|
||||
let mut output:Option<u8> = None;
|
||||
|
||||
if let Some(new_bit) = possible_bit
|
||||
{
|
||||
self.since_bit = 0;
|
||||
self.byte |= (new_bit as u8).reverse_bits() >> self.bit;
|
||||
self.bit += 1;
|
||||
|
||||
if self.bit >= 8
|
||||
{
|
||||
output = Some(self.byte);
|
||||
self.byte = 0x00;
|
||||
self.bit = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.since_bit += 1;
|
||||
|
||||
if self.since_bit > self.abandon
|
||||
{
|
||||
self.byte = 0x00;
|
||||
self.bit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Demodulator
|
||||
{
|
||||
squelch: i16,
|
||||
|
||||
symbol_rise: Vec<i16>, // symbol templates; chirps from lower_freq to upper_freq and vice versa, encoding 1 and 0
|
||||
symbol_fall: Vec<i16>,
|
||||
|
||||
symbol_rise_norm: i64, // the absolute sum values of the symbol templates, used for normalizing the correlations
|
||||
symbol_fall_norm: i64,
|
||||
|
||||
signal_buffer: posi::Buffer<i16>, // shift buffer holding incoming samples to be correlated with the symbol templates
|
||||
signal_sum: i64, // absolute sum of the signal buffer, used to determine average signal level
|
||||
|
||||
correl_rise_lopass: gauss::Filter, // low-pass filters for the two correlation outputs
|
||||
correl_fall_lopass: gauss::Filter,
|
||||
|
||||
correl_rise_hipass: gauss::Filter, // high-pass filters for the two correlation outputs
|
||||
correl_fall_hipass: gauss::Filter,
|
||||
|
||||
correl_rise_buffer: posi::Buffer<i64>, // buffers holding the filtered correlation outputs for analysis
|
||||
correl_fall_buffer: posi::Buffer<i64>,
|
||||
|
||||
correl_rise_maxima: Vec<i64>, // lists used to keep track of the maximum value currently present in the correlation output buffers
|
||||
correl_fall_maxima: Vec<i64>,
|
||||
|
||||
correl_peak_average: i64, // running average height of a peak in the correlation buffers, used to reject spurious small peaks
|
||||
|
||||
accumulator_byte: u8, // a byte that we are collecting from bits (symbols) before returning when it is complete
|
||||
accumulator_bit: u8, // which bit we want next to assemble a byte (0=MSB)
|
||||
samples_since_bit: usize, // how long it has been since a symbol was last detected
|
||||
abandon_samples: usize, // how long to wait for the next bit before discarding accumulated bits and starting a new byte
|
||||
signal_input: SignalInput,
|
||||
correl_lopass: CorrelFilter,
|
||||
correl_hipass: CorrelFilter,
|
||||
correl_output: CorrelOutput,
|
||||
accumulator: ByteAccumulator,
|
||||
}
|
||||
|
||||
impl Demodulator
|
||||
{
|
||||
pub fn new
|
||||
(
|
||||
center_freq: f32, // center frequency of the signal, referenced to the Nyquist limit
|
||||
deviation: f32, // half-width of the signal chirps, referenced to the Nyquist limit. (note: actual bandwidth will be higher due to sidebands.)
|
||||
symbol_len: f32, // length of a symbol (a chirp) in samples
|
||||
) ->
|
||||
center_freq: f32,
|
||||
deviation: f32,
|
||||
symbol_length: f32,
|
||||
) ->
|
||||
Demodulator
|
||||
{
|
||||
let lower_freq = (center_freq - deviation).max(0.0).min(1.0);
|
||||
let upper_freq = (center_freq + deviation).max(0.0).min(1.0);
|
||||
|
||||
// important!!
|
||||
// since the posi buffers shift from low to high index, their signal contents is reversed.
|
||||
// we need to reverse our templates, too, or our correlations will be backwards and produce inverted data.
|
||||
|
||||
// the signals are, in fact, just mirror images of each other, but we're coding this
|
||||
// as though they are not in case this changes later.
|
||||
let symbol_rise:Vec<i16> = chirp::generate(upper_freq, lower_freq, symbol_len);
|
||||
let symbol_fall:Vec<i16> = chirp::generate(lower_freq, upper_freq, symbol_len);
|
||||
|
||||
let symbol_rise_norm:i64 =
|
||||
(
|
||||
symbol_rise
|
||||
.iter()
|
||||
.map(
|
||||
|&x| (x as i64).abs()
|
||||
)
|
||||
.sum()
|
||||
);
|
||||
let symbol_fall_norm:i64 =
|
||||
(
|
||||
symbol_fall
|
||||
.iter()
|
||||
.map(
|
||||
|&x| (x as i64).abs()
|
||||
)
|
||||
.sum()
|
||||
);
|
||||
|
||||
let signal_bufsize = symbol_len.round() as usize;
|
||||
let correl_bufsize = (symbol_len * 1.0).round() as usize; // longer than 1.0*symbol_len may or may not be a good idea?
|
||||
|
||||
Demodulator
|
||||
{
|
||||
squelch: 0,
|
||||
|
||||
symbol_rise: symbol_rise,
|
||||
symbol_fall: symbol_fall,
|
||||
|
||||
symbol_rise_norm: symbol_rise_norm,
|
||||
symbol_fall_norm: symbol_fall_norm,
|
||||
|
||||
signal_buffer: posi::Buffer::new(signal_bufsize),
|
||||
signal_sum: 0,
|
||||
|
||||
correl_rise_lopass: gauss::Filter::new(deviation / 2.0),
|
||||
correl_fall_lopass: gauss::Filter::new(deviation / 2.0),
|
||||
|
||||
correl_rise_hipass: gauss::Filter::new(deviation / 16.0),
|
||||
correl_fall_hipass: gauss::Filter::new(deviation / 16.0),
|
||||
|
||||
correl_rise_buffer: posi::Buffer::new(correl_bufsize),
|
||||
correl_fall_buffer: posi::Buffer::new(correl_bufsize),
|
||||
|
||||
correl_rise_maxima: Vec::new(),
|
||||
correl_fall_maxima: Vec::new(),
|
||||
|
||||
correl_peak_average: 0,
|
||||
|
||||
accumulator_byte: 0x00,
|
||||
accumulator_bit: 0,
|
||||
samples_since_bit: 0,
|
||||
abandon_samples: (symbol_len * 4.0 / 3.0).round() as usize,
|
||||
signal_input: SignalInput::new(center_freq, deviation, symbol_length),
|
||||
correl_lopass: CorrelFilter::new(deviation / 2.0),
|
||||
correl_hipass: CorrelFilter::new(deviation / 8.0),
|
||||
correl_output: CorrelOutput::new(symbol_length.round() as usize),
|
||||
accumulator: ByteAccumulator::new((symbol_length * 4.0 / 3.0).round() as usize),
|
||||
}
|
||||
}
|
||||
|
||||
// set the minimum signal level (absolute average) for demodulation to proceed
|
||||
pub fn set_squelch
|
||||
(
|
||||
&mut self,
|
||||
new_value: u16,
|
||||
) {
|
||||
self.squelch = (new_value / 2) as i16;
|
||||
}
|
||||
|
||||
// basically a wrapper around signal_buffer.shift_in() that also updates signal_sum
|
||||
fn ingest_signal
|
||||
pub fn process
|
||||
(
|
||||
&mut self,
|
||||
new_sample: i16,
|
||||
) {
|
||||
let old_sample = self.signal_buffer.last();
|
||||
self.signal_sum -= (old_sample as i64).abs();
|
||||
self.signal_sum += (new_sample as i64).abs();
|
||||
|
||||
self.signal_buffer.shift_in(new_sample);
|
||||
}
|
||||
|
||||
// does the correlation of the signal with the templates
|
||||
// also passes the result through the filters and shifts it into the correlation output buffers
|
||||
fn correlate
|
||||
(
|
||||
&mut self,
|
||||
) {
|
||||
let local_signal = self.signal_buffer.to_vec();
|
||||
|
||||
let mut correl_rise:i64 = 0;
|
||||
let mut correl_fall:i64 = 0;
|
||||
|
||||
for (&signal_value, &rise_value) in
|
||||
(
|
||||
local_signal.iter()
|
||||
).zip(
|
||||
self.symbol_rise.iter()
|
||||
) {
|
||||
correl_rise +=
|
||||
(
|
||||
signal_value as i64
|
||||
*
|
||||
rise_value as i64
|
||||
);
|
||||
}
|
||||
|
||||
for (&signal_value, &fall_value) in
|
||||
(
|
||||
local_signal.iter()
|
||||
).zip(
|
||||
self.symbol_fall.iter()
|
||||
) {
|
||||
correl_fall +=
|
||||
(
|
||||
signal_value as i64
|
||||
*
|
||||
fall_value as i64
|
||||
);
|
||||
}
|
||||
|
||||
correl_rise *= 32767;
|
||||
correl_rise /= self.symbol_rise_norm;
|
||||
|
||||
correl_fall *= 32767;
|
||||
correl_fall /= self.symbol_fall_norm;
|
||||
|
||||
correl_rise = correl_rise.abs();
|
||||
correl_fall = correl_fall.abs();
|
||||
|
||||
correl_rise = self.correl_rise_lopass.lo_process(correl_rise);
|
||||
correl_fall = self.correl_fall_lopass.lo_process(correl_fall);
|
||||
|
||||
correl_rise = self.correl_rise_hipass.hi_process(correl_rise);
|
||||
correl_fall = self.correl_fall_hipass.hi_process(correl_fall);
|
||||
|
||||
let old_rise = self.correl_rise_buffer.last();
|
||||
let old_fall = self.correl_fall_buffer.last();
|
||||
|
||||
if let Ok(index) = self.correl_rise_maxima.binary_search(&old_rise)
|
||||
{
|
||||
self.correl_rise_maxima.remove(index);
|
||||
}
|
||||
if let Ok(index) = self.correl_fall_maxima.binary_search(&old_fall)
|
||||
{
|
||||
self.correl_fall_maxima.remove(index);
|
||||
}
|
||||
|
||||
if (
|
||||
self.correl_rise_maxima.len() == 0
|
||||
||
|
||||
Some(&correl_rise) >= self.correl_rise_maxima.last()
|
||||
) {
|
||||
self.correl_rise_maxima.push(correl_rise);
|
||||
}
|
||||
if (
|
||||
self.correl_fall_maxima.len() == 0
|
||||
||
|
||||
Some(&correl_fall) >= self.correl_fall_maxima.last()
|
||||
) {
|
||||
self.correl_fall_maxima.push(correl_fall);
|
||||
}
|
||||
|
||||
self.correl_rise_buffer.shift_in(correl_rise);
|
||||
self.correl_fall_buffer.shift_in(correl_fall);
|
||||
}
|
||||
|
||||
// for the two output buffers, determines if the current midpoint is both
|
||||
// a) the maximum value in the buffer
|
||||
// b) higher, or nearly higher, than the average peak height (e.g. not a spurious small peak)
|
||||
fn detect_peaks
|
||||
(
|
||||
&mut self,
|
||||
) ->
|
||||
(Option<i64>, Option<i64>)
|
||||
{
|
||||
let rise_value = self.correl_rise_buffer.mid();
|
||||
let fall_value = self.correl_fall_buffer.mid();
|
||||
|
||||
let mut have_rise:Option<i64> = None;
|
||||
let mut have_fall:Option<i64> = None;
|
||||
|
||||
if (
|
||||
rise_value > 0
|
||||
&&
|
||||
Some(&rise_value) == self.correl_rise_maxima.last()
|
||||
) {
|
||||
if self.correl_peak_average <= 0
|
||||
{
|
||||
self.correl_peak_average = rise_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.correl_peak_average = (rise_value + self.correl_peak_average * 15) / 16;
|
||||
}
|
||||
|
||||
if rise_value > self.correl_peak_average * 15 / 16
|
||||
{
|
||||
have_rise = Some(rise_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
fall_value > 0
|
||||
&&
|
||||
Some(&fall_value) == self.correl_fall_maxima.last()
|
||||
) {
|
||||
if self.correl_peak_average <= 0
|
||||
{
|
||||
self.correl_peak_average = fall_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.correl_peak_average = (fall_value + self.correl_peak_average * 15) / 16;
|
||||
}
|
||||
|
||||
if fall_value > self.correl_peak_average * 15 / 16
|
||||
{
|
||||
have_fall = Some(fall_value);
|
||||
}
|
||||
}
|
||||
|
||||
return (have_rise, have_fall);
|
||||
}
|
||||
|
||||
// determines if we actually have a symbol right now, and if so, emits its value
|
||||
fn determine
|
||||
(
|
||||
&mut self,
|
||||
) ->
|
||||
Option<bool>
|
||||
Option<u8>
|
||||
{
|
||||
if (self.signal_sum / self.signal_buffer.len() as i64) < self.squelch as i64
|
||||
{
|
||||
// note that this means all the demodulation stuff (namely the correlation) still happens all the time,
|
||||
// it's just that if the signal is below the squelch threshold, then symbol detections are suppressed.
|
||||
return None;
|
||||
}
|
||||
self.signal_input.ingest(new_sample);
|
||||
let (correl_rise, correl_fall) = self.signal_input.process();
|
||||
|
||||
let (have_rise, have_fall) = self.detect_peaks();
|
||||
self.correl_lopass.ingest(correl_rise.abs(), correl_fall.abs());
|
||||
let (rise_lo, fall_lo) = self.correl_lopass.lo_process();
|
||||
|
||||
if have_rise.is_some() && have_fall.is_none()
|
||||
{
|
||||
return Some(true);
|
||||
}
|
||||
if have_fall.is_some() && have_rise.is_none()
|
||||
{
|
||||
return Some(false);
|
||||
}
|
||||
self.correl_hipass.ingest(rise_lo, fall_lo);
|
||||
let (rise_hi, fall_hi) = self.correl_hipass.hi_process();
|
||||
|
||||
return None;
|
||||
self.correl_output.ingest(rise_hi, fall_hi);
|
||||
let possible_bit = self.correl_output.have_bit();
|
||||
|
||||
let possible_byte = self.accumulator.accumulate(possible_bit);
|
||||
|
||||
return possible_byte;
|
||||
}
|
||||
|
||||
pub fn set_squelch
|
||||
(
|
||||
&mut self,
|
||||
value: u16,
|
||||
) {
|
||||
self.correl_output.squelch = (value / 2) as i64;
|
||||
}
|
||||
|
||||
// debugging method that emits the current values of the correlation buffer midpoints
|
||||
pub fn dump_correl
|
||||
(
|
||||
&self,
|
||||
|
@ -316,51 +528,8 @@ impl Demodulator
|
|||
(i64, i64)
|
||||
{
|
||||
(
|
||||
self.correl_rise_buffer.mid(),
|
||||
self.correl_fall_buffer.mid(),
|
||||
self.correl_output.buffer_rise.mid(),
|
||||
self.correl_output.buffer_fall.mid(),
|
||||
)
|
||||
}
|
||||
|
||||
// primary outer method, which takes in samples and emits bytes if there are any.
|
||||
pub fn process
|
||||
(
|
||||
&mut self,
|
||||
new_sample: i16,
|
||||
) ->
|
||||
Option<u8>
|
||||
{
|
||||
self.ingest_signal(new_sample);
|
||||
|
||||
self.correlate();
|
||||
|
||||
if let Some(bit) = self.determine()
|
||||
{
|
||||
self.accumulator_byte |= (bit as u8).reverse_bits() >> self.accumulator_bit;
|
||||
self.accumulator_bit += 1;
|
||||
|
||||
self.samples_since_bit = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.samples_since_bit += 1;
|
||||
|
||||
if self.samples_since_bit > self.abandon_samples
|
||||
{
|
||||
self.accumulator_byte = 0x00;
|
||||
self.accumulator_bit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut result:Option<u8> = None;
|
||||
|
||||
if self.accumulator_bit >= 8
|
||||
{
|
||||
result = Some(self.accumulator_byte);
|
||||
|
||||
self.accumulator_byte = 0x00;
|
||||
self.accumulator_bit = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,14 @@ impl Enmodulator
|
|||
) ->
|
||||
Vec<i16>
|
||||
{
|
||||
let mut new_signal:Vec<i16> = Vec::with_capacity(8*self.symbol_len + 2*self.guard_pad);
|
||||
let mut new_signal:Vec<i16> =
|
||||
(
|
||||
Vec::with_capacity(
|
||||
(8 * self.symbol_len)
|
||||
+
|
||||
(2 * self.guard_pad)
|
||||
)
|
||||
);
|
||||
|
||||
new_signal.append(&mut vec![0; self.guard_pad]);
|
||||
|
||||
|
|
137
src/gauss.rs
137
src/gauss.rs
|
@ -1,137 +0,0 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
use crate::posi;
|
||||
|
||||
fn gaussian
|
||||
(
|
||||
width: f32,
|
||||
) ->
|
||||
Vec<i64>
|
||||
{
|
||||
let radius = (width * 9.4).round().max(1.0) as isize;
|
||||
|
||||
let mut output:Vec<i64> = Vec::with_capacity(radius as usize * 2 + 1);
|
||||
|
||||
for t in
|
||||
(
|
||||
(
|
||||
-radius
|
||||
..=
|
||||
radius
|
||||
).map(
|
||||
|x| x as f32
|
||||
)
|
||||
) {
|
||||
output.push(
|
||||
(
|
||||
32768.0 *
|
||||
(
|
||||
-0.5 * (t / width).powi(2)
|
||||
)
|
||||
.exp()
|
||||
/
|
||||
(
|
||||
width * (2.0 * PI).sqrt()
|
||||
)
|
||||
)
|
||||
.round()
|
||||
as i64
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
pub struct Filter
|
||||
{
|
||||
kernel: Vec<i64>,
|
||||
buffer: posi::Buffer<i64>,
|
||||
}
|
||||
|
||||
impl Filter
|
||||
{
|
||||
pub fn new
|
||||
(
|
||||
frequency: f32,
|
||||
) ->
|
||||
Filter
|
||||
{
|
||||
let width:f32 = 1.0 / (PI * frequency);
|
||||
let kernel = gaussian(width);
|
||||
let buffer = posi::Buffer::new(kernel.len());
|
||||
|
||||
Filter
|
||||
{
|
||||
kernel: kernel,
|
||||
buffer: buffer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hi_process
|
||||
(
|
||||
&mut self,
|
||||
new_value: i64,
|
||||
) ->
|
||||
i64
|
||||
{
|
||||
self.buffer.shift_in(new_value);
|
||||
|
||||
let mut output_value:i64 = 0;
|
||||
|
||||
for (&buffer_value, &kernel_value) in
|
||||
(
|
||||
self.buffer.to_vec().iter()
|
||||
).zip(
|
||||
self.kernel.iter()
|
||||
) {
|
||||
output_value +=
|
||||
(
|
||||
buffer_value
|
||||
*
|
||||
kernel_value
|
||||
);
|
||||
}
|
||||
|
||||
output_value /= 32768;
|
||||
|
||||
output_value =
|
||||
(
|
||||
self.buffer.mid() as i64
|
||||
-
|
||||
output_value
|
||||
);
|
||||
|
||||
return output_value;
|
||||
}
|
||||
|
||||
pub fn lo_process
|
||||
(
|
||||
&mut self,
|
||||
new_value: i64,
|
||||
) ->
|
||||
i64
|
||||
{
|
||||
self.buffer.shift_in(new_value);
|
||||
|
||||
let mut output_value:i64 = 0;
|
||||
|
||||
for (&buffer_value, &kernel_value) in
|
||||
(
|
||||
self.buffer.to_vec().iter()
|
||||
).zip(
|
||||
self.kernel.iter()
|
||||
) {
|
||||
output_value +=
|
||||
(
|
||||
buffer_value
|
||||
*
|
||||
kernel_value
|
||||
);
|
||||
}
|
||||
|
||||
output_value /= 32768;
|
||||
|
||||
return output_value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
pub fn generate
|
||||
(
|
||||
freq: f32,
|
||||
) ->
|
||||
Vec<i64>
|
||||
{
|
||||
let width = 1.0 / (PI * freq);
|
||||
let radius = (width * 9.4).round().max(1.0) as isize;
|
||||
|
||||
let mut output:Vec<i64> = Vec::with_capacity(radius as usize * 2 + 1);
|
||||
|
||||
for t in
|
||||
(
|
||||
(
|
||||
-radius
|
||||
..=
|
||||
radius
|
||||
).map(
|
||||
|x| x as f32
|
||||
)
|
||||
) {
|
||||
output.push(
|
||||
(
|
||||
32768.0 *
|
||||
(
|
||||
-0.5 * (t / width).powi(2)
|
||||
)
|
||||
.exp()
|
||||
/
|
||||
(
|
||||
width * (2.0 * PI).sqrt()
|
||||
)
|
||||
)
|
||||
.round()
|
||||
as i64
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
|
@ -11,7 +11,7 @@ use std::process::exit;
|
|||
extern crate lapp;
|
||||
|
||||
mod chirp;
|
||||
mod gauss;
|
||||
mod gaussian;
|
||||
mod posi;
|
||||
mod enmodulation;
|
||||
mod demodulation;
|
||||
|
@ -39,7 +39,7 @@ fn main
|
|||
-b, --bit-rate (default 50.0) - Data rate, in chirps (bits) per second. Be aware that lower values will increase system load.
|
||||
-f, --center-frequency (default 4000.0) - Center frequency of the signal in Hertz.
|
||||
-d, --deviation (default 2000.0) - Maximum range traversed above and below the center frequency by the signal.
|
||||
-t, --squelch (default 20) - Minimum signal amplitude for demodulation, from 0 (accept all) to 65535 (ignore all).
|
||||
-t, --squelch (default 4000) - Minimum signal match degree for demodulation, from 0 (accept all) to 65535 (ignore all).
|
||||
-a, --ascii-only - When demodulating, suppress all output that is not an ASCII character. Possibly useful if searching for text on a noisy channel.
|
||||
|
||||
-g, --debug (default 0) - If specified when demodulating, emits every nth correlation value in f32 form to threnodyne_rise.dat and threnodyne_fall.dat.
|
||||
|
|
Loading…
Reference in New Issue