301 lines
8.3 KiB
Rust
301 lines
8.3 KiB
Rust
#![allow(unused_parens)]
|
|
|
|
use std::io::prelude::*;
|
|
use std::io::ErrorKind;
|
|
use std::io::BufReader;
|
|
use std::fs::File;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
use std::process::exit;
|
|
|
|
extern crate lapp;
|
|
|
|
mod chirp;
|
|
mod gaussian;
|
|
mod posi;
|
|
mod enmodulation;
|
|
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 16-bit little-endian integer sample format.
|
|
Data input/output is arbitrary binary data, which can be text, file contents, or whatever else.
|
|
|
|
Usage:
|
|
<mode> (string) - Either 'e' to encode/enmodulate, 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 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 0.4) - Minimum signal match degree for demodulation, from 0.0 (accept all) to 1.0 (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.
|
|
"
|
|
)
|
|
);
|
|
|
|
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");
|
|
let arg_squelch = args.get_float("squelch");
|
|
let arg_ascii = args.get_bool("ascii-only");
|
|
let arg_debug = args.get_integer("debug");
|
|
|
|
if !(
|
|
&["e", "d"].contains(&mode.as_str())
|
|
) {
|
|
eprintln!("threnodyne: please specify either 'e' to encode/enmodulate 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);
|
|
}
|
|
|
|
if arg_squelch < 0.0
|
|
{
|
|
eprintln!("threnodyne: the squelch threshold must be at least 0.0 ({} won't do)", arg_squelch);
|
|
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 enmodulator = enmodulation::Enmodulator::new(center_freq, deviation, symbol_len);
|
|
|
|
let mut output_buffer:Vec<u8> = Vec::with_capacity(symbol_size * 2);
|
|
|
|
let wall_start = Instant::now();
|
|
let mut process_time = Duration::new(0,0);
|
|
let mut samples_processed:usize = 0;
|
|
let mut bytes_processed:usize = 0;
|
|
|
|
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 iter_time = Instant::now();
|
|
|
|
let signal = enmodulator.process(byte);
|
|
|
|
output_buffer.clear();
|
|
|
|
for &sample in signal.iter()
|
|
{
|
|
output_buffer.extend_from_slice(
|
|
&(
|
|
(sample * 32767.0) as i16
|
|
)
|
|
.to_le_bytes()
|
|
);
|
|
}
|
|
|
|
process_time += iter_time.elapsed();
|
|
bytes_processed += 1;
|
|
samples_processed += signal.len();
|
|
|
|
match output_file.write_all(&output_buffer)
|
|
{
|
|
Err(why) =>
|
|
{
|
|
eprintln!("threnodyne: output write error: {}", why);
|
|
exit(1);
|
|
},
|
|
Ok(_) => (),
|
|
};
|
|
}
|
|
|
|
eprintln!();
|
|
eprintln!("samples processed: {}", samples_processed);
|
|
eprintln!("wall time: {} ms", wall_start.elapsed().as_millis());
|
|
eprintln!("process time: {} ms", process_time.as_millis());
|
|
eprintln!("process rate: {} samples/s, {} bytes/s", samples_processed as f32 / process_time.as_secs_f32(), bytes_processed as f32 / process_time.as_secs_f32());
|
|
}
|
|
|
|
if mode.as_str() == "d"
|
|
{
|
|
let mut demodulator = demodulation::Demodulator::new(center_freq, deviation, symbol_len);
|
|
|
|
demodulator.set_squelch(arg_squelch);
|
|
|
|
let wall_start = Instant::now();
|
|
let mut process_time = Duration::new(0,0);
|
|
let mut samples_processed:usize = 0;
|
|
let mut bytes_processed:usize = 0;
|
|
|
|
let mut debug_files:Option<(File,File)> = None;
|
|
if arg_debug > 0
|
|
{
|
|
debug_files =
|
|
Some(
|
|
(
|
|
match File::create("threnodyne_rise.dat")
|
|
{
|
|
Ok(file) => file,
|
|
Err(why) =>
|
|
{
|
|
eprintln!("threnodyne: could not create debug output file for rising correlation: {}", why);
|
|
exit(1);
|
|
},
|
|
},
|
|
match File::create("threnodyne_fall.dat")
|
|
{
|
|
Ok(file) => file,
|
|
Err(why) =>
|
|
{
|
|
eprintln!("threnodyne: could not create debug output file for falling correlation: {}", why);
|
|
exit(1);
|
|
},
|
|
},
|
|
)
|
|
);
|
|
}
|
|
|
|
let mut ending:bool = false;
|
|
let mut ending_counter:usize = symbol_len as usize;
|
|
|
|
loop
|
|
{
|
|
let mut sample_bytes:[u8;2] = [0x00, 0x00];
|
|
if !ending
|
|
{
|
|
match input_reader.read_exact(&mut sample_bytes)
|
|
{
|
|
Err(why) if why.kind() == ErrorKind::UnexpectedEof =>
|
|
{
|
|
ending = true
|
|
},
|
|
Err(why) =>
|
|
{
|
|
eprintln!("threnodyne: input read error: {}", why);
|
|
exit(1);
|
|
},
|
|
Ok(_) => (),
|
|
}
|
|
}
|
|
else if ending_counter > 0
|
|
{
|
|
ending_counter -= 1;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
let iter_time = Instant::now();
|
|
|
|
let sample =
|
|
(
|
|
i16::from_le_bytes(sample_bytes) as f32
|
|
/
|
|
32767.0
|
|
);
|
|
let demod_result = demodulator.process(sample);
|
|
|
|
if let Some((risefile, fallfile)) = &mut debug_files
|
|
{
|
|
if samples_processed % (arg_debug as usize) == 0
|
|
{
|
|
let (riseval, fallval) = demodulator.dump_correl();
|
|
risefile.write_all(&riseval.to_be_bytes()).expect("write failed on rise debug file");
|
|
fallfile.write_all(&fallval.to_be_bytes()).expect("write failed on fall debug file");
|
|
}
|
|
}
|
|
|
|
process_time += iter_time.elapsed();
|
|
samples_processed += 1;
|
|
|
|
if let Some(byte) = demod_result
|
|
{
|
|
bytes_processed += 1;
|
|
if byte.is_ascii() || !arg_ascii
|
|
{
|
|
match output_file.write_all(&[byte])
|
|
{
|
|
Err(why) =>
|
|
{
|
|
eprintln!("threnodyne: output write error: {}", why);
|
|
exit(1);
|
|
},
|
|
Ok(_) => (),
|
|
};
|
|
|
|
let _ = output_file.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
eprintln!();
|
|
eprintln!("samples processed: {}", samples_processed);
|
|
eprintln!("bytes processed: {}", bytes_processed);
|
|
eprintln!("wall time: {} ms", wall_start.elapsed().as_millis());
|
|
eprintln!("process time: {} ms", process_time.as_millis());
|
|
eprintln!("process rate: {} samples/s, {} bytes/s", samples_processed as f32 / process_time.as_secs_f32(), bytes_processed as f32 / process_time.as_secs_f32());
|
|
}
|
|
}
|