initial commit; preliminary working state

This commit is contained in:
Fluora Eigenwire 2020-03-08 09:28:02 -05:00
commit 5fffcbce43
10 changed files with 744 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

16
Cargo.lock generated Normal file
View File

@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "lapp"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "threnodyne"
version = "0.1.0"
dependencies = [
"lapp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum lapp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60bf485afeba9437a275ad29a9383b03f2978450e7feceffb55be8c0dbad9829"

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "threnodyne"
version = "0.1.0"
authors = ["Fluora Eigendiode <ellie.martin.eberhardt@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lapp = "0.4.0"

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# Threnodyne
#### More audio-based data transmission! This time it's using chirp spread spectrum
### What this is
Threnodyne is another thing for making a sound card work as a silly budget SDR, in the same vein as
[transgride](https://tildegit.org/fluora/transgride). To work on radio waves, it probably needs some
kind of upconverter, unless you're into VLF.
The use case we have in mind is just plugging it into a radio set that is designed to handle audio.
We are also hoping it will work for other audio data encoding purposes, like storing data on
cassette tapes, or even transmitting data between computers using sound.
### How it works
Modulation is simple: you simply select an audio frequency center and deviation, and the program
generates a series of chirps - tones with linearly-changing frequency - centered on the specified
frequency and traversing the specified deviation. A rising chirp encodes a 1 bit, and a falling
chirp encodes a 0 bit.
Demodulation is accomplished by taking the cross-correlation of the template chirp signals with the
incoming received signal, then searching for the sharp spikes in either of the results which
correspond to the two possible symbols.
### How to use it
As usual for Rust projects, you can build it (after installing the toolchain) with
`cargo build --release`, which will emit an executable into `target/release`. Run that executable
with `--help` for detailed usage information.
### Is it useful?
Maybe? The use case we have in mind is relatively low-rate (a handful of bytes per second) digital
radio communication, mainly for using voice-oriented radios as text-mode transceivers. This will not
be especially useful for *fast* data transfer, but may help when trying to cram data through a janky
channel with heavy interference and tight bandwidth limits.

57
src/chirp.rs Normal file
View File

@ -0,0 +1,57 @@
use std::f32::consts::PI;
// this function is used by both the modulator and demodulator to generate chirps, so it's split off here
// to ensure that it is always exactly the same for both modules.
pub fn generate
(
freq_lo: f32, // 1.0 = Nyquist limit
freq_hi: f32,
length: f32, // samples
) ->
Vec<i16>
{
let freq_delta = (freq_hi - freq_lo) / (length * 2.0);
let env_center = length / 2.0;
let env_falloff = env_center / 4.8;
let mut new_chirp:Vec<i16> = Vec::with_capacity(length.round() as usize);
for i in 0..(length.round() as usize)
{
let t = i as f32;
let env:f32 =
(
(
-0.5 *
(
(t - env_center) / env_falloff
)
.powi(2)
)
.exp()
);
new_chirp.push(
(
32000.0 *
env *
(
PI * t *
(
freq_lo
+
(t * freq_delta)
)
)
.sin()
)
.round()
as i16
);
}
return new_chirp;
}

241
src/demodulation.rs Normal file
View File

@ -0,0 +1,241 @@
#![allow(unused_parens)]
use crate::posi;
use crate::chirp;
pub struct Demodulator
{
symbol_rise: Vec<i16>, // chirp from lower_freq to upper_freq, encoding a 1
symbol_fall: Vec<i16>, // chirp from upper_freq to lower_freq, encoding a 0
signal_bufsize: usize,
signal_buffer: posi::Buffer<i16>, // buffer holding the incoming signal to be convolved with the rising and falling chirps
signal_abs_sum: i64,
correl_bufsize: usize,
correl_rise_buffer: posi::Buffer<i64>, // buffer holding the values of the convolution with the rising chirp
correl_rise_abs_sum: i64,
correl_rise_abs_max: Vec<i64>, // structure for tracking the absolute maximum value currently contained in this buffer
correl_fall_buffer: posi::Buffer<i64>, // buffer holding the values of the convolution with the falling chirp
correl_fall_abs_sum: i64,
correl_fall_abs_max: Vec<i64>,
holdoff: usize,
}
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 center frequency. (note: actual bandwidth will be higher due to sidebands.)
symbol_len: f32, // length of a symbol (a chirp) in samples
) ->
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);
// 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 mut symbol_rise:Vec<i16> = chirp::generate(lower_freq, upper_freq, symbol_len);
let mut symbol_fall:Vec<i16> = chirp::generate(upper_freq, lower_freq, symbol_len);
// 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 convolutions will be backwards and produce inverted data.
symbol_rise.reverse();
symbol_fall.reverse();
let signal_bufsize = symbol_len.round() as usize;
let correl_bufsize = (symbol_len * 0.9).round() as usize;
Demodulator
{
symbol_rise: symbol_rise,
symbol_fall: symbol_fall,
signal_bufsize: signal_bufsize,
signal_buffer: posi::Buffer::new(signal_bufsize),
signal_abs_sum: 0,
correl_bufsize: correl_bufsize,
correl_rise_buffer: posi::Buffer::new(correl_bufsize),
correl_rise_abs_sum: 0,
correl_rise_abs_max: Vec::new(),
correl_fall_buffer: posi::Buffer::new(correl_bufsize),
correl_fall_abs_sum: 0,
correl_fall_abs_max: Vec::new(),
holdoff: correl_bufsize,
}
}
fn ingest
(
&mut self,
new_sample: i16,
) {
let old_sample = self.signal_buffer.last();
self.signal_abs_sum += new_sample.abs() as i64;
self.signal_abs_sum -= old_sample.abs() as i64;
self.signal_buffer.shift_in(new_sample);
if self.signal_abs_sum == 0
{
self.holdoff = self.signal_bufsize;
}
}
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()
) {
// this results in a value normalized to 32767^2 * length
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
);
}
let (new_value_rise, new_value_fall) = (correl_rise, correl_fall);
let (old_value_rise, old_value_fall) = (self.correl_rise_buffer.last(), self.correl_fall_buffer.last());
self.correl_rise_abs_sum += new_value_rise.abs();
self.correl_rise_abs_sum -= old_value_rise.abs();
self.correl_fall_abs_sum += new_value_fall.abs();
self.correl_fall_abs_sum -= old_value_fall.abs();
if let Some(index) = self.correl_rise_abs_max.iter().position(|x| x == &old_value_rise.abs())
{
self.correl_rise_abs_max.remove(index);
}
if let Some(index) = self.correl_fall_abs_max.iter().position(|x| x == &old_value_fall.abs())
{
self.correl_fall_abs_max.remove(index);
}
if (
new_value_rise >= *self.correl_rise_abs_max.last().unwrap_or(&0)
&&
new_value_rise != 0
) {
self.correl_rise_abs_max.push(new_value_rise);
}
if (
new_value_fall >= *self.correl_fall_abs_max.last().unwrap_or(&0)
&&
new_value_fall != 0
) {
self.correl_fall_abs_max.push(new_value_fall);
}
self.correl_rise_buffer.shift_in(new_value_rise);
self.correl_fall_buffer.shift_in(new_value_fall);
}
fn determine
(
&self,
) ->
Option<bool>
{
let rise_avg = (self.correl_rise_abs_sum / self.correl_bufsize as i64);
let fall_avg = (self.correl_fall_abs_sum / self.correl_bufsize as i64);
// these values will stay at zero if no interval-maximum is found for their corresponding symbol template.
// otherwise, they will be set to that symbol's convolution magnitude.
let mut rise_weight:i64 = 0;
let mut fall_weight:i64 = 0;
let current_rise = self.correl_rise_buffer.mid().abs();
if (
current_rise > rise_avg * 4
&&
self.correl_rise_abs_max.last() == Some(&current_rise)
) {
rise_weight = current_rise;
}
let current_fall = self.correl_fall_buffer.mid().abs();
if (
current_fall > fall_avg * 4
&&
self.correl_fall_abs_max.last() == Some(&current_fall)
) {
fall_weight = current_fall;
}
if rise_weight != 0 || fall_weight != 0
{
return Some(fall_weight < rise_weight);
} else {
return None;
}
}
pub fn process
(
&mut self,
new_sample: i16,
) ->
Option<bool>
{
// general principle of operation:
// 1. convolve input signal with 1-symbol and 0-symbol template signals.
// 2. divide the convolution values by the total magnitude of the input signal over the convolution interval.
// 3. continuously check the normalized convolution curve for peaks which are, for an interval of one
// symbol-length centered on the peak in question, both:
// a. the tallest peak in the interval.
// b. at least three times the average value in the interval.
// 4. upon finding such a peak for either symbol template, emit a bit of demodulated data.
// if peaks coincide for both symbols, emit whichever bit has a higher convolution value.
self.ingest(new_sample);
self.correlate();
if self.holdoff > 0
{
self.holdoff -= 1;
return None;
} else {
return self.determine();
}
}
}

199
src/main.rs Normal file
View File

@ -0,0 +1,199 @@
#![allow(unused_parens)]
use std::io::prelude::*;
use std::io::ErrorKind;
use std::io::BufReader;
use std::process::exit;
extern crate lapp;
mod chirp;
mod posi;
mod modulation;
mod demodulation;
fn main
() {
let args =
(
lapp::parse_args(
"
Threnodyne is program for transmitting and receiving data in chirp-sequence spread-spectrum form
using your computer's audio card.
Chirps can provide substantial resistance to unfavorable channel conditions, which makes this program
potentially suitable for transmitting data over channels intended for narrowband voice communication.
Audio input/output is in raw 16-bit little-endian PCM format.
Data input/output is arbitrary binary data, which can be text, file contents, or whatever else.
Usage:
<mode> (string) - Either 'e' to encode/modulate, or 'd' to decode/demodulate.
-i, --input (default stdin) - Filelike object from which to read input.
-o, --output (default stdout) - Filelike object into which to write output.
-s, --sample-rate (default 48000.0) - Sample rate in Hertz, used for calculating modulation frequencies
-b, --bit-rate (default 480.0) - Data rate, in chirps (bits) per second. Be aware that lower values will increase system load.
-f, --center-frequency (default 12000.0) - Center frequency of the signal in Hertz.
-d, --deviation (default 1000.0) - Maximum distance traversed above and below the center frequency by the signal.
"
)
);
let mode = args.get_string("mode");
let mut input_reader = BufReader::new(args.get_infile("input"));
let mut output_file = args.get_outfile("output");
let arg_samplerate = args.get_float("sample-rate");
let arg_bitrate= args.get_float("bit-rate");
let arg_centerfreq = args.get_float("center-frequency");
let arg_deviation = args.get_float("deviation");
if !(
&["e", "d"].contains(&mode.as_str())
) {
eprintln!("threnodyne: please specify either 'e' to encode/modulate or 'd' to decode/demodulate.");
exit(1);
}
if arg_samplerate <= 0.0
{
eprintln!("threnodyne: the sample rate must be a positive, nonzero number ({} won't do)", arg_samplerate);
exit(1);
}
if arg_bitrate < 0.0
{
eprintln!("threnodyne: the bitrate must be a positive number ({} won't do)", arg_bitrate);
exit(1);
}
if arg_bitrate > arg_samplerate
{
eprintln!("threnodyne: the bitrate cannot be larger than the sample rate (a bitrate of {} won't do when the sample rate is {})", arg_bitrate, arg_samplerate);
exit(1);
}
if arg_centerfreq < 0.0
{
eprintln!("threnodyne: the center frequency must be a positive number ({} won't do)", arg_centerfreq);
exit(1);
}
if arg_deviation < 0.0
{
eprintln!("threnodyne: the frequency deviation must be a positive number ({} won't do)", arg_deviation);
exit(1);
}
if arg_deviation > arg_centerfreq
{
eprintln!("threnodyne: the frequency deviation cannot be larger than the center frequency (a deviation of {} won't do when the center frequency is {})", arg_deviation, arg_centerfreq);
exit(1);
}
let symbol_len = arg_samplerate / arg_bitrate;
let symbol_size = symbol_len.round() as usize;
let center_freq = 2.0 * arg_centerfreq / arg_samplerate;
let deviation = 2.0 * arg_deviation / arg_samplerate;
if mode.as_str() == "e"
{
let modulator = modulation::Modulator::new(center_freq, deviation, symbol_len);
let mut output_buffer:Vec<u8> = Vec::with_capacity(symbol_size);
loop
{
let mut input_buffer:[u8;1] = [0x00];
let byte = match input_reader.read_exact(&mut input_buffer)
{
Err(why) if why.kind() == ErrorKind::UnexpectedEof => break,
Err(why) =>
{
eprintln!("threnodyne: input read error: {}", why);
exit(1);
},
Ok(_) => input_buffer[0],
};
let signal = modulator.process(byte);
output_buffer.clear();
for &sample in signal.iter()
{
output_buffer.extend_from_slice(&sample.to_le_bytes());
}
match output_file.write_all(&output_buffer)
{
Err(why) =>
{
eprintln!("threnodyne: output write error: {}", why);
exit(1);
},
Ok(_) => (),
};
}
}
if mode.as_str() == "d"
{
let mut demodulator = demodulation::Demodulator::new(center_freq, deviation, symbol_len);
let mut accumulator_byte:u8 = 0x00;
let mut accumulator_bit:u8 = 0;
let mut samples_since_bit:usize = 0;
let guard_pad = symbol_size / 16;
loop
{
let mut sample_bytes:[u8;2] = [0x00, 0x00];
match input_reader.read_exact(&mut sample_bytes)
{
Err(why) if why.kind() == ErrorKind::UnexpectedEof => break,
Err(why) =>
{
eprintln!("threnodyne: input read error: {}", why);
exit(1);
},
Ok(_) => (),
}
let sample = i16::from_le_bytes(sample_bytes);
if let Some(bit) = demodulator.process(sample)
{
accumulator_byte |= [0b0000_0000, 0b1000_0000] [bit as usize] >> accumulator_bit;
accumulator_bit += 1;
samples_since_bit = 0;
} else {
samples_since_bit += 1;
if samples_since_bit >= (symbol_size + guard_pad)
{
accumulator_bit = 0;
accumulator_byte = 0x00;
}
}
if accumulator_bit >= 8
{
match output_file.write_all(&[accumulator_byte])
{
Err(why) =>
{
eprintln!("threnodyne: output write error: {}", why);
exit(1);
},
Ok(_) => (),
};
accumulator_bit = 0;
accumulator_byte = 0x00;
let _ = output_file.flush();
}
}
}
}

73
src/modulation.rs Normal file
View File

@ -0,0 +1,73 @@
#![allow(unused_parens)]
use crate::chirp;
pub struct Modulator
{
symbol_len: usize,
guard_pad: usize,
symbol_rise: Vec<i16>,
symbol_fall: Vec<i16>,
}
impl Modulator
{
pub fn new
(
center_freq: f32,
deviation: f32,
symbol_len: f32,
) ->
Modulator
{
let lower_freq = (center_freq - deviation).max(0.0).min(1.0);
let upper_freq = (center_freq + deviation).max(0.0).min(1.0);
let symbol_rise:Vec<i16> = chirp::generate(lower_freq, upper_freq, symbol_len);
let symbol_fall:Vec<i16> = chirp::generate(upper_freq, lower_freq, symbol_len);
Modulator
{
symbol_len: symbol_len.round() as usize,
guard_pad: (symbol_len / 16.0).round() as usize,
symbol_rise: symbol_rise,
symbol_fall: symbol_fall,
}
}
pub fn process
(
&self,
byte: u8,
) ->
Vec<i16>
{
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]);
for i in 0..8
{
new_signal.extend_from_slice(
[
&self.symbol_fall,
&self.symbol_rise,
] [
(
(
byte
&
(0b1000_0000 >> i)
)
!= 0
)
as usize
]
);
}
new_signal.append(&mut vec![0; self.guard_pad]);
return new_signal;
}
}

104
src/posi.rs Normal file
View File

@ -0,0 +1,104 @@
#![allow(dead_code)]
#[derive(Clone)]
pub struct Buffer<T>
where T: Copy + Clone + Default
{
data: Vec<T>, // the actual data in the buffer; length never changes after construction
front: usize, // where in the data vec is the first element?
bound: usize, // len-1; modulo-length can be done very fast by just bitwise-anding with this, since the length is 2^n
size: usize, // len; separate from bound just to make the code look nicer
}
impl<T> Buffer<T>
where T: Copy + Clone + Default
{
pub fn new
(
size: usize,
) ->
Buffer<T>
{
if size == 0
{
panic!("attempt to create a posi::Buffer with zero size");
}
let true_size = size.next_power_of_two();
Buffer {
data: vec![Default::default(); true_size],
front: 0,
bound: true_size - 1,
size: size,
}
}
pub fn shift_in
(
&mut self,
input: T,
) {
self.front = self.front.wrapping_sub(1);
self.front &= self.bound;
unsafe
{
*self.data.get_unchecked_mut(self.front) = input;
}
}
pub fn get
(
&self,
index: usize,
) ->
T
{
unsafe
{
*self.data.get_unchecked((self.front + index) & self.bound)
}
}
pub fn mid
(
&self,
) ->
T
{
self.get(self.size / 2)
}
pub fn last
(
&self,
) ->
T
{
self.get(self.size - 1)
}
pub fn len
(
&self,
) ->
usize
{
self.size
}
pub fn to_vec
(
&self
) ->
Vec<T>
{
let mut output:Vec<T> = Vec::with_capacity(self.data.len());
let (upper_slice, lower_slice) = self.data.split_at(self.front);
output.extend_from_slice(lower_slice);
output.extend_from_slice(upper_slice);
output.truncate(self.size);
return output;
}
}

9
test.txt Normal file
View File

@ -0,0 +1,9 @@
THIS IS NOT A PLACE OF HONOR
NO HIGHLY ESTEEMED DEED IS COMMEMORATED HERE
NOTHING VALUED IS HERE
THIS PLACE IS A MESSAGE AND PART OF A SYSTEM OF MESSAGES
PAY ATTENTION TO IT
SENDING THIS MESSAGE WAS IMPORTANT TO US
WE CONSIDERED OURSELVES TO BE A POWERFUL CULTURE