Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

19 changed files with 354 additions and 3412 deletions

View File

@ -1,18 +0,0 @@
on: push
jobs:
audit:
runs-on: docker
container:
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: audit-apt-decoder
- run: CARGO_HOME=/root/.cargo cargo audit

View File

@ -1,55 +0,0 @@
on: push
jobs:
build:
runs-on: docker
container:
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: build-apt-decoder
restore-keys: audit-apt-decoder
- run: CARGO_HOME=~/.cargo cargo build --release
build-windows:
runs-on: docker
container:
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
needs: build
steps:
- uses: actions/checkout@v3
- uses: actions/cache/restore@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: build-apt-decoder
- run: CARGO_HOME=~/.cargo cargo build --target x86_64-pc-windows-gnu --release
build-appimage:
runs-on: docker
container:
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
needs: build
steps:
- uses: actions/checkout@v3
- uses: actions/cache/restore@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: build-apt-decoder
- run: CARGO_HOME=~/.cargo PATH=$PATH:$CARGO_HOME/bin x build -r --format appimage

View File

@ -1,31 +0,0 @@
on:
push:
tags: 'v*'
jobs:
publish-release:
runs-on: docker
container:
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
steps:
- uses: actions/checkout@v3
- uses: actions/cache/restore@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: build-apt-decoder
- run: CARGO_HOME=~/.cargo cargo build --target x86_64-pc-windows-gnu --release
- run: CARGO_HOME=~/.cargo PATH=$PATH:$CARGO_HOME/bin x build -r --format appimage
- run: mkdir -p release
- run: cp target/x/release/linux/x64/apt-decoder.AppImage release/apt-decoder-${{ github.ref_name }}.AppImage
- run: cp target/x86_64-pc-windows-gnu/release/apt-decoder.exe release/
- run: cd release && zip apt-decoder-win.zip apt-decoder.exe && rm apt-decoder.exe
- uses: actions/forgejo-release@v1
with:
direction: upload
release-dir: release
token: ${{ secrets.FORGEJO_RELEASE }}

3
.gitignore vendored
View File

@ -1,4 +1 @@
target target
.vendor
*.AppImage
icon.png

2810
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,8 @@
[package] [package]
name = "apt-decoder" name = "apt-decoder"
version = "1.0.1" version = "0.1.0"
authors = ["Sebastian <sebastian@sebastians-site.de>"] authors = ["Sebastian <sebastian@sebastians-site.de>"]
[dependencies] [dependencies]
clap = {version = "4.0.0", features = ["cargo"]} hound = "*"
indicatif = "0.16.2" image = "*"
hound = "3.4.0"
image = "0.24.0"
eframe = {version = "0.18.0", optional = true}
rfd = "0.11.4"
thiserror = "1.0.30"
[features]
# Defines a feature named `webp` that does not enable any other features.
default = ["ui"]
ui = ["eframe"]

BIN
gui.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 420 KiB

View File

@ -9,45 +9,28 @@ apt-decoder provides a lightweight, simple to use and easy to understand solutio
![short sample](noaa19_short.png) ![short sample](noaa19_short.png)
Releases
--------
You can download the latest release as a standalone binary,
for linux (appimage) and windows (just a statically linked exe file) under
https://gitea.zenerdio.de/sebastian/apt-decoder/releases
Building Building
-------- --------
1. Install the development packages for `libgtk3` and `libxcb` for your distro. 1. Install the rust compiler and cargo.
E.g. for anything Debian based:
`sudo apt install libgtk-3-dev libxcb-shape0-dev libxcb-xfixes0-dev`
2. Install the rust compiler and cargo.
E.g. using rustup (Try installing rustup using yourpackage manager, E.g. using rustup (Try installing rustup using yourpackage manager,
don't use the stupid `curl | sh` stuff.) don't use the stupid **curl | sh** stuff.)
3. Run `cargo build --release` 2. Run `cargo build --release`
4. The `apt-decoder` binary is in `target/release` 3. The `apt-decoder` binary is in `target/release`
5. Done 4. Done
The default build will build a binary that contains,
both the GUI and the CLI version of the tool.
If you need something more lightweight (with no external dependencies),
it is also possible to build a pure-rust CLI-only version,
using `cargo build --release --no-default-features`.
Usage Usage
----- -----
1. Save a received and FM-demodulated satellite signal as WAV-file. 1. Save a received and FM-demodulated satellite signal as WAV-file.
The WAV file has to be **mono**, **48kHz** and **32bit float**. The WAV file has to be **mono**, **48kHz** and **32bit float**.
When in doubt you can use audacity to convert your file into this format. When in doubt you can use audacity to convert your file into this format.
2. To run `apt-decoder` in GUI mode just execute the binary. 2. Run `apt-decoder <your WAV file> <destination PNG file>`
For CLI mode use the `-n` flag: You can also run the example contained in this repo:
`apt-decoder -n <your WAV file> <destination PNG file>` `apt-decoder noaa19_short.wav noaa19_short.png`
For testing you can try the example contained in this repo:
`apt-decoder -n noaa19_short.wav noaa19_short.png`
3. Look at the generated PNG file, adjust the dynamic and contrast with your favorite tool. 3. Look at the generated PNG file, adjust the dynamic and contrast with your favorite tool.
4. Done 4. Done
![gui example](gui.png)
![long sample](example.png) ![long sample](example.png)
Theory of Operation Theory of Operation

View File

@ -1,12 +1,9 @@
pub struct SquaringAMDemodulator<'a> { pub struct SquaringAMDemodulator<'a> {
iterator: Box<dyn Iterator<Item = f32> + 'a>, iterator: Box<Iterator<Item=f32> + 'a>,
} }
impl<'a> SquaringAMDemodulator<'a> { impl<'a> SquaringAMDemodulator<'a> {
pub fn from<I>(iterator1: I) -> SquaringAMDemodulator<'a> pub fn from<I>(iterator1: I) -> SquaringAMDemodulator<'a> where I: Iterator<Item=f32> + 'a {
where
I: Iterator<Item = f32> + 'a,
{
SquaringAMDemodulator { SquaringAMDemodulator {
iterator: Box::new(iterator1), iterator: Box::new(iterator1),
} }
@ -19,7 +16,7 @@ impl<'a> Iterator for SquaringAMDemodulator<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.iterator.next() { match self.iterator.next() {
Some(x) => Some((x * x).sqrt()), Some(x) => Some((x * x).sqrt()),
None => None, None => None
} }
} }
} }

View File

@ -1,25 +1,30 @@
const SYNC_LENGHT : usize = 40; const SYNC_LENGHT : usize = 40;
const SYNCA_SEQ: [bool; 40] = [ const SYNCA_SEQ : [bool; 40] = [false, false, false, false,
false, false, false, false, true, true, false, false, // Pulse 1 true, true, false, false, // Pulse 1
true, true, false, false, // Pulse 2 true, true, false, false, // Pulse 2
true, true, false, false, // Pulse 3 true, true, false, false, // Pulse 3
true, true, false, false, // Pulse 4 true, true, false, false, // Pulse 4
true, true, false, false, // Pulse 5 true, true, false, false, // Pulse 5
true, true, false, false, // Pulse 6 true, true, false, false, // Pulse 6
true, true, false, false, // Pulse 7 true, true, false, false, // Pulse 7
false, false, false, false, false, false, false, false, false, false, false, false,
]; false, false, false, false,];
const SYNCB_SEQ : [bool; 40] = [false, false, false, false,
true, true, true, false, false,
true, true, true, false, false,
true, true, true, false, false,
true, true, true, false, false,
true, true, true, false, false,
true, true, true, false, false,
true, true, true, false, false,
false];
const SYNCB_SEQ: [bool; 40] = [
false, false, false, false, true, true, true, false, false, true, true, true, false, false,
true, true, true, false, false, true, true, true, false, false, true, true, true, false, false,
true, true, true, false, false, true, true, true, false, false, false,
];
pub enum SyncedSample { pub enum SyncedSample {
Sample(f32), Sample(f32),
SyncA(f32), SyncA(f32),
SyncB(f32), SyncB(f32)
} }
pub struct APTSyncer<'a> { pub struct APTSyncer<'a> {
@ -27,14 +32,11 @@ pub struct APTSyncer<'a> {
pos : usize, pos : usize,
nones_read: usize, nones_read: usize,
avg_level : f32, avg_level : f32,
iterator: Box<dyn Iterator<Item = f32> + 'a>, iterator: Box<Iterator<Item=f32> + 'a>
} }
impl<'a> APTSyncer<'a> { impl<'a> APTSyncer<'a> {
pub fn from<I>(mut iterator: I) -> APTSyncer<'a> pub fn from<I>(mut iterator: I) -> APTSyncer<'a> where I: Iterator<Item=f32> + 'a {
where
I: Iterator<Item = f32> + 'a,
{
let mut state = [0.0; SYNC_LENGHT]; let mut state = [0.0; SYNC_LENGHT];
let mut avg_level = 0.5; let mut avg_level = 0.5;
for i in 0..SYNC_LENGHT { for i in 0..SYNC_LENGHT {
@ -42,17 +44,17 @@ impl<'a> APTSyncer<'a> {
Some(x) => { Some(x) => {
state[i] = x; state[i] = x;
avg_level = 0.25 * x + avg_level * 0.75; avg_level = 0.25 * x + avg_level * 0.75;
} },
None => panic!("Could not retrieve enough samples to prime syncer"), None => panic!("Could not retrieve enough samples to prime syncer")
} }
} }
APTSyncer { APTSyncer {
state, state: state,
pos: 0, pos: 0,
nones_read: 0, nones_read: 0,
avg_level, avg_level: avg_level,
iterator: Box::new(iterator), iterator: Box::new(iterator)
} }
} }
@ -85,6 +87,7 @@ impl<'a> Iterator for APTSyncer<'a> {
type Item = SyncedSample; type Item = SyncedSample;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let (is_a, is_b) = self.is_marker(); let (is_a, is_b) = self.is_marker();
let sample = self.state[self.pos]; let sample = self.state[self.pos];
@ -92,8 +95,8 @@ impl<'a> Iterator for APTSyncer<'a> {
Some(x) => { Some(x) => {
self.state[self.pos] = x; self.state[self.pos] = x;
self.avg_level = 0.25 * x + self.avg_level * 0.75; self.avg_level = 0.25 * x + self.avg_level * 0.75;
} },
None => self.nones_read += 1, None => self.nones_read += 1
}; };
if self.nones_read >= SYNC_LENGHT { if self.nones_read >= SYNC_LENGHT {
@ -104,9 +107,11 @@ impl<'a> Iterator for APTSyncer<'a> {
if is_a { if is_a {
return Some(SyncedSample::SyncA(sample)); return Some(SyncedSample::SyncA(sample));
} else if is_b { }
else if is_b {
return Some(SyncedSample::SyncB(sample)); return Some(SyncedSample::SyncB(sample));
} else { }
else {
return Some(SyncedSample::Sample(sample)); return Some(SyncedSample::Sample(sample));
} }
} }

View File

@ -1,26 +0,0 @@
use indicatif::{ProgressBar, ProgressStyle};
use decoder;
const STEPS: u64 = 100;
pub fn decode(input_path: &str, output_path: &str) {
println!("Decoding {} to {}", input_path, output_path);
let bar = ProgressBar::new(STEPS).with_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{wide_bar}] {percent}% ({eta})")
.progress_chars("=> "),
);
let res = decoder::decode(input_path, output_path, |progress, _| {
bar.set_position((progress * STEPS as f32) as u64);
(true, STEPS as u32)
});
bar.finish();
if let Err(error) = res {
println!("Unable to decode file: {}", error);
} else {
println!("Done!")
}
}

View File

@ -1,197 +0,0 @@
use std::path::Path;
use amdemod::SquaringAMDemodulator;
use aptsyncer::{APTSyncer, SyncedSample};
use errors::DecoderError;
use firfilter::FIRFilter;
use resamplers::{Downsampler, Upsampler};
use utils::float_sample_iterator;
const LINES_PER_SECOND: u32 = 2;
const PIXELS_PER_LINE: u32 = 2080;
const LOWPASS_COEFFS: [f32; 63] = [
-7.383784e-03,
-3.183046e-03,
2.255039e-03,
7.461166e-03,
1.091908e-02,
1.149109e-02,
8.769802e-03,
3.252932e-03,
-3.720606e-03,
-1.027446e-02,
-1.447403e-02,
-1.486427e-02,
-1.092423e-02,
-3.307958e-03,
6.212477e-03,
1.511364e-02,
2.072873e-02,
2.096037e-02,
1.492345e-02,
3.347624e-03,
-1.138407e-02,
-2.560252e-02,
-3.507114e-02,
-3.591225e-02,
-2.553830e-02,
-3.371569e-03,
2.882645e-02,
6.711368e-02,
1.060042e-01,
1.394643e-01,
1.620650e-01,
1.700462e-01,
1.620650e-01,
1.394643e-01,
1.060042e-01,
6.711368e-02,
2.882645e-02,
-3.371569e-03,
-2.553830e-02,
-3.591225e-02,
-3.507114e-02,
-2.560252e-02,
-1.138407e-02,
3.347624e-03,
1.492345e-02,
2.096037e-02,
2.072873e-02,
1.511364e-02,
6.212477e-03,
-3.307958e-03,
-1.092423e-02,
-1.486427e-02,
-1.447403e-02,
-1.027446e-02,
-3.720606e-03,
3.252932e-03,
8.769802e-03,
1.149109e-02,
1.091908e-02,
7.461166e-03,
2.255039e-03,
-3.183046e-03,
-7.383784e-03,
];
pub fn decode<T>(
input_file: &str,
output_file: &str,
progress_update: T,
) -> Result<(), DecoderError>
where
T: Fn(f32, image::RgbaImage) -> (bool, u32),
{
let mut reader =
hound::WavReader::open(input_file).map_err(|err| DecoderError::InputFileError(err))?;
if reader.spec().channels != 1 {
panic!("Expected a mono file");
}
let sample_rate = reader.spec().sample_rate;
if sample_rate != 48000 {
return Err(DecoderError::UnexpectedSamplingRate(sample_rate));
}
let sample_count = reader.len();
let seconds = (sample_count as f32) / (sample_rate as f32);
let lines = (seconds.ceil() as u32) * LINES_PER_SECOND;
let mut img = image::DynamicImage::ImageLuma8(image::ImageBuffer::new(PIXELS_PER_LINE, lines));
let coeffs = &LOWPASS_COEFFS;
let samples = float_sample_iterator(&mut reader);
let demod = SquaringAMDemodulator::from(samples);
let filter = FIRFilter::from(demod, coeffs);
let upsampler = Upsampler::from(filter, 13);
let downsampler = Downsampler::from(upsampler, 150);
let syncer = APTSyncer::from(downsampler);
let mut x = 0;
let mut y = 0;
let mut max_level = 0.0;
let mut has_sync = false;
let mut progress = 0;
let pixel_count = sample_count * 13 / 150;
let mut update_step = 10;
let mut previous_sample = 0.0;
for synced_sample in syncer {
progress += 1;
let sample = match synced_sample {
SyncedSample::Sample(s) => s,
SyncedSample::SyncA(s) => {
if !has_sync {
max_level = 0.0;
has_sync = true;
}
x = 0;
s
}
SyncedSample::SyncB(s) => {
if x < (PIXELS_PER_LINE / 2) {
let skip_distance = (PIXELS_PER_LINE / 2) - x;
let color = (previous_sample / max_level * 255.0) as u8;
for i in 0..skip_distance {
img.as_mut_luma8()
.unwrap()
.put_pixel(x + i, y, image::Luma([color]));
}
}
if !has_sync {
max_level = 0.0;
has_sync = true;
}
x = PIXELS_PER_LINE / 2;
s
}
};
max_level = f32::max(sample, max_level);
let color = (sample / max_level * 255.0) as u8;
if y < lines {
img.as_mut_luma8()
.unwrap()
.put_pixel(x, y, image::Luma([color]));
}
x += 1;
if x >= PIXELS_PER_LINE {
x = 0;
y += 1;
}
previous_sample = sample;
if progress % (PIXELS_PER_LINE * update_step) == 0 {
let (cont, update_steps) =
progress_update((progress as f32) / (pixel_count as f32), img.to_rgba8());
if !cont {
return Ok(());
}
let line_count = pixel_count / PIXELS_PER_LINE;
update_step = if line_count / update_steps > 4 {
line_count / update_steps
} else {
4
}
}
}
progress_update(1.0, img.to_rgba8());
img.save_with_format(&Path::new(output_file), image::ImageFormat::Png)
.map_err(|err| DecoderError::OutputFileError(err))?;
Ok(())
}

View File

@ -1,15 +0,0 @@
use hound;
use image;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DecoderError {
#[error("Unable to read input file: {0}")]
InputFileError(#[from] hound::Error),
#[error("Expected a sampling rate of 48000Hz not {0}Hz")]
UnexpectedSamplingRate(u32),
#[error("Unable to write output file: {0}")]
OutputFileError(#[from] image::ImageError),
}

View File

@ -2,24 +2,21 @@ pub struct FIRFilter<'a> {
coeffs: &'a [f32], coeffs: &'a [f32],
state: Vec<f32>, state: Vec<f32>,
pos: usize, pos: usize,
iterator: Box<dyn Iterator<Item = f32> + 'a>, iterator: Box<Iterator<Item=f32> + 'a>
} }
impl<'a> FIRFilter<'a> { impl<'a> FIRFilter<'a> {
pub fn from<I>(iterator: I, coeffs: &'a [f32]) -> FIRFilter<'a> pub fn from<I>(iterator: I, coeffs: &'a [f32]) -> FIRFilter<'a> where I: Iterator<Item=f32> + 'a {
where
I: Iterator<Item = f32> + 'a,
{
let mut state = Vec::new(); let mut state = Vec::new();
for _ in 0..coeffs.len() { for _ in 0..coeffs.len() {
state.push(0.0); state.push(0.0);
} }
FIRFilter { FIRFilter {
coeffs, coeffs: coeffs,
state, state: state,
pos: 0, pos: 0,
iterator: Box::new(iterator), iterator: Box::new(iterator)
} }
} }
} }
@ -30,7 +27,7 @@ impl<'a> Iterator for FIRFilter<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let cur = match self.iterator.next() { let cur = match self.iterator.next() {
Some(x) => x, Some(x) => x,
None => return None, None => return None
}; };
self.pos = (self.pos + 1) % self.coeffs.len(); self.pos = (self.pos + 1) % self.coeffs.len();
@ -40,7 +37,7 @@ impl<'a> Iterator for FIRFilter<'a> {
for i in 0..self.coeffs.len() { for i in 0..self.coeffs.len() {
let pos = (self.pos + self.coeffs.len() - i) % self.coeffs.len(); let pos = (self.pos + self.coeffs.len() - i) % self.coeffs.len();
result += self.state[pos] * self.coeffs[i]; result += self.state[pos] * self.coeffs[i];
} };
Some(result) Some(result)
} }

View File

@ -1,76 +1,153 @@
#![windows_subsystem = "windows"]
extern crate clap;
extern crate hound; extern crate hound;
extern crate image; extern crate image;
extern crate indicatif;
extern crate rfd;
extern crate thiserror;
#[cfg(feature = "ui")] mod utils;
extern crate eframe;
mod amdemod;
mod aptsyncer;
mod cli;
mod decoder;
mod errors;
mod firfilter; mod firfilter;
mod resamplers; mod resamplers;
mod utils; mod amdemod;
mod aptsyncer;
use clap::{arg, command}; use std::io::prelude::*;
use std::fs::File;
use std::path::Path;
use utils::float_sample_iterator;
use firfilter::FIRFilter;
use amdemod::SquaringAMDemodulator;
use resamplers::{Upsampler, Downsampler};
use aptsyncer::{APTSyncer, SyncedSample};
const LINES_PER_SECOND: u32 = 2;
const PIXELS_PER_LINE: u32 = 2080;
const LOWPASS_COEFFS : [f32; 63] = [ -7.383784e-03,
-3.183046e-03, 2.255039e-03, 7.461166e-03, 1.091908e-02,
1.149109e-02, 8.769802e-03, 3.252932e-03, -3.720606e-03,
-1.027446e-02, -1.447403e-02, -1.486427e-02, -1.092423e-02,
-3.307958e-03, 6.212477e-03, 1.511364e-02, 2.072873e-02,
2.096037e-02, 1.492345e-02, 3.347624e-03, -1.138407e-02,
-2.560252e-02, -3.507114e-02, -3.591225e-02, -2.553830e-02,
-3.371569e-03, 2.882645e-02, 6.711368e-02, 1.060042e-01,
1.394643e-01, 1.620650e-01, 1.700462e-01, 1.620650e-01,
1.394643e-01, 1.060042e-01, 6.711368e-02, 2.882645e-02,
-3.371569e-03, -2.553830e-02, -3.591225e-02, -3.507114e-02,
-2.560252e-02, -1.138407e-02, 3.347624e-03, 1.492345e-02,
2.096037e-02, 2.072873e-02, 1.511364e-02, 6.212477e-03,
-3.307958e-03, -1.092423e-02, -1.486427e-02, -1.447403e-02,
-1.027446e-02, -3.720606e-03, 3.252932e-03, 8.769802e-03,
1.149109e-02, 1.091908e-02, 7.461166e-03, 2.255039e-03,
-3.183046e-03, -7.383784e-03];
#[cfg(not(feature = "ui"))]
fn main() { fn main() {
let matches = command!() let args : Vec<String> = std::env::args().collect();
.arg(arg!([wavfile] "Input wav file with 48kHz samplingrate").required(true))
.arg(arg!([pngfile] "Output png file").default_value("output.png"))
.get_matches();
let input_file = matches if args.len() != 3 {
.get_one::<String>("wavfile") println!("Usage: {} <wav file> <output file>", args[0]);
.expect("No input file given"); return;
let output_file = matches
.get_one::<String>("pngfile")
.expect("No output file given");
cli::decode(&input_file, &output_file);
} }
#[cfg(feature = "ui")] let mut reader = match hound::WavReader::open(&args[1]) {
mod ui; Err(e) => panic!("Could not open inputfile: {}", e),
Ok(r) => r
};
#[cfg(feature = "ui")] if reader.spec().channels != 1 {
use ui::DecoderApp; panic!("Expected a mono file");
}
#[cfg(feature = "ui")] let sample_rate = reader.spec().sample_rate;
fn main() { println!("Samplerate: {}", sample_rate);
let matches = command!() if sample_rate != 48000 {
.arg(arg!([wavfile] "Input wav file with 48kHz samplingrate").default_value("input.wav")) panic!("Expected a 48kHz sample rate");
.arg(arg!([pngfile] "Output png file").default_value("output.png")) }
.arg(arg!(-n --nogui "Disable gui and run in command line mode"))
.get_matches();
let input_file = matches let sample_count = reader.len();
.get_one::<String>("wavfile") let seconds = (sample_count as f32) / (sample_rate as f32);
.expect("No input file given") let lines = (seconds.ceil() as u32) * LINES_PER_SECOND;
.to_string(); println!("File contains {} seconds or {} lines", seconds, lines);
let output_file = matches
.get_one::<String>("pngfile")
.expect("No output file given")
.to_string();
if matches.get_flag("nogui") { let mut img = image::ImageBuffer::new(PIXELS_PER_LINE, lines);
cli::decode(&input_file, &output_file);
} else {
let native_options = eframe::NativeOptions::default();
eframe::run_native( let coeffs = &LOWPASS_COEFFS;
"APT-Decoder",
native_options, let samples = float_sample_iterator(&mut reader);
Box::new(move |_cc| Box::new(DecoderApp::new(&input_file, &output_file))),
); let demod = SquaringAMDemodulator::from(samples);
let filter = FIRFilter::from(demod, coeffs);
let upsampler = Upsampler::from(filter, 13);
let downsampler = Downsampler::from(upsampler, 150);
let syncer = APTSyncer::from(downsampler);
let mut x = 0;
let mut y = 0;
let mut max_level = 0.0;
let mut has_sync = false;
let mut progress = 0;
let step = sample_count * 13 / 150 / 10;
let mut previous_sample = 0.0;
print!("0%");
std::io::stdout().flush().unwrap();
for synced_sample in syncer {
progress += 1;
if progress % step == 0 {
print!("...{}%", progress / step * 10);
std::io::stdout().flush().unwrap();
}
let sample = match synced_sample {
SyncedSample::Sample(s) => s,
SyncedSample::SyncA(s) =>{
if !has_sync {
max_level = 0.0;
has_sync = true;
}
x = 0;
s
}
SyncedSample::SyncB(s) =>{
if x < (PIXELS_PER_LINE / 2) {
let skip_distance = (PIXELS_PER_LINE / 2) - x;
let color = (previous_sample / max_level * 255.0) as u8;
for i in 0..skip_distance {
img.put_pixel(x + i,y,image::Luma([color]));
} }
} }
if !has_sync {
max_level = 0.0;
has_sync = true;
}
x = PIXELS_PER_LINE / 2;
s
}
};
max_level = f32::max(sample, max_level);
let color = (sample / max_level * 255.0) as u8;
if y < lines {
img.put_pixel(x,y,image::Luma([color]));
}
x += 1;
if x >= PIXELS_PER_LINE {
x = 0;
y += 1;
}
previous_sample = sample;
}
println!("");
let ref mut fout = match File::create(&Path::new(&args[2])) {
Err(e) => panic!("Could not open outputfile: {}", e),
Ok(f) => f
};
image::ImageLuma8(img).save(fout, image::PNG).unwrap();
println!("Done !");
}

View File

@ -1,18 +1,15 @@
pub struct Upsampler<'a> { pub struct Upsampler<'a> {
factor: u16, factor: u16,
state: u16, state: u16,
iterator: Box<dyn Iterator<Item = f32> + 'a>, iterator: Box<Iterator<Item=f32> + 'a>
} }
impl<'a> Upsampler<'a> { impl<'a> Upsampler<'a> {
pub fn from<I>(iterator: I, factor: u16) -> Upsampler<'a> pub fn from<I>(iterator: I, factor: u16) -> Upsampler<'a> where I: Iterator<Item=f32> + 'a {
where
I: Iterator<Item = f32> + 'a,
{
Upsampler { Upsampler {
factor: factor, factor: factor,
state: 0, state: 0,
iterator: Box::new(iterator), iterator: Box::new(iterator)
} }
} }
} }
@ -23,7 +20,8 @@ impl<'a> Iterator for Upsampler<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let result = if self.state == 0 { let result = if self.state == 0 {
self.iterator.next() self.iterator.next()
} else { }
else {
Some(0.0) Some(0.0)
}; };
self.state = (self.state + 1) % self.factor; self.state = (self.state + 1) % self.factor;
@ -32,19 +30,18 @@ impl<'a> Iterator for Upsampler<'a> {
} }
} }
pub struct Downsampler<'a> { pub struct Downsampler<'a> {
factor: u16, factor: u16,
iterator: Box<dyn Iterator<Item = f32> + 'a>, iterator: Box<Iterator<Item=f32> + 'a>
} }
impl<'a> Downsampler<'a> { impl<'a> Downsampler<'a> {
pub fn from<I>(iterator: I, factor: u16) -> Downsampler<'a> pub fn from<I>(iterator: I, factor: u16) -> Downsampler<'a> where I: Iterator<Item=f32> + 'a {
where
I: Iterator<Item = f32> + 'a,
{
Downsampler { Downsampler {
factor: factor, factor: factor,
iterator: Box::new(iterator), iterator: Box::new(iterator)
} }
} }
} }
@ -57,7 +54,7 @@ impl<'a> Iterator for Downsampler<'a> {
for _ in 0..self.factor { for _ in 0..self.factor {
match self.iterator.next() { match self.iterator.next() {
Some(x) => result += x, Some(x) => result += x,
None => return None, None => return None
} }
} }
result /= self.factor as f32; result /= self.factor as f32;

193
src/ui.rs
View File

@ -1,193 +0,0 @@
use std::sync::{Arc, Mutex};
use eframe::egui;
use eframe::egui::text_edit::TextEdit;
use eframe::egui::widgets::{Button, ProgressBar};
use eframe::egui::ColorImage;
use eframe::egui::Visuals;
use eframe::egui::{Color32, RichText};
use decoder;
use errors::DecoderError;
#[derive(PartialEq)]
enum DecoderRunState {
RUNNING,
CANCELED,
DONE,
}
struct DecoderJobState {
update_steps: u32,
progress: f32,
texture: Option<egui::TextureHandle>,
run_state: DecoderRunState,
error: Option<DecoderError>,
}
impl DecoderJobState {
fn is_running(&self) -> bool {
self.run_state == DecoderRunState::RUNNING
}
}
impl Default for DecoderJobState {
fn default() -> Self {
Self {
update_steps: 10,
progress: 0.0,
texture: None,
run_state: DecoderRunState::DONE,
error: None,
}
}
}
pub struct DecoderApp {
input_path: String,
output_path: String,
decoding_state: Arc<Mutex<DecoderJobState>>,
}
impl DecoderApp {
pub fn new(input_path: &str, output_path: &str) -> Self {
Self {
input_path: input_path.to_owned(),
output_path: output_path.to_owned(),
decoding_state: Arc::new(Mutex::new(DecoderJobState::default())),
}
}
}
impl eframe::App for DecoderApp {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self {
input_path,
output_path,
decoding_state,
} = self;
{
let mut state = decoding_state.lock().unwrap();
if !ctx.input().raw.dropped_files.is_empty() && !state.is_running() {
if let Some(path) = ctx.input().raw.dropped_files[0].clone().path {
*input_path = path.display().to_string();
}
}
ctx.set_visuals(Visuals::dark());
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("APT-Decoder");
egui::Grid::new("form_grid").num_columns(3).show(ui, |ui| {
ui.label("Input Wav File:");
ui.add_sized(
[300.0, 20.0],
TextEdit::singleline(input_path).interactive(!state.is_running()),
);
if ui
.add_enabled(!state.is_running(), Button::new("Open"))
.clicked()
{
if let Some(path) = rfd::FileDialog::new().pick_file() {
*input_path = path.display().to_string();
}
};
ui.end_row();
ui.label("Output PNG File:");
ui.add_sized(
[300.0, 20.0],
TextEdit::singleline(output_path).interactive(!state.is_running()),
);
if ui
.add_enabled(!state.is_running(), Button::new("Save"))
.clicked()
{
if let Some(path) = rfd::FileDialog::new().save_file() {
*output_path = path.display().to_string();
}
};
ui.end_row();
});
ui.horizontal(|ui| {
if ui
.add_enabled(!state.is_running(), Button::new("Decode"))
.clicked()
{
let ctx = ctx.clone();
let decoding_state = decoding_state.clone();
let input_path = input_path.clone();
let output_path = output_path.clone();
state.error = None;
state.run_state = DecoderRunState::RUNNING;
state.texture = None;
std::thread::spawn(move || {
let decoder_res =
decoder::decode(&input_path, &output_path, |progress, image| {
let mut state = decoding_state.lock().unwrap();
state.progress = progress;
let size = [image.width() as _, image.height() as _];
let color_img = ColorImage::from_rgba_unmultiplied(
size,
image.as_flat_samples().as_slice(),
);
state.texture =
Some(ctx.load_texture("decoded-image", color_img));
ctx.request_repaint();
return (state.is_running(), state.update_steps);
});
let mut state = decoding_state.lock().unwrap();
state.run_state = DecoderRunState::DONE;
state.error = match decoder_res {
Err(err) => Some(err),
_ => None,
};
ctx.request_repaint();
});
}
if ui
.add_enabled(
state.is_running(),
Button::new(RichText::new("Cancel").color(Color32::RED)),
)
.clicked()
{
state.run_state = DecoderRunState::CANCELED;
}
});
let progressbar = ProgressBar::new(state.progress).show_percentage();
ui.add(progressbar);
ui.end_row();
if let Some(err) = &state.error {
ui.label(RichText::new(err.to_string()).color(Color32::RED));
};
ui.separator();
let image_size = ui.available_size();
state.update_steps = image_size[1] as u32;
if let Some(texture) = &state.texture {
ui.image(texture, image_size);
}
});
}
}
}

View File

@ -4,28 +4,15 @@ extern crate hound;
type FileReader = std::io::BufReader<std::fs::File>; type FileReader = std::io::BufReader<std::fs::File>;
pub fn float_sample_iterator<'a>( pub fn float_sample_iterator<'a>(reader: &'a mut hound::WavReader<FileReader>)
reader: &'a mut hound::WavReader<FileReader>, -> Box<Iterator<Item=f32> + 'a> {
) -> Box<dyn Iterator<Item = f32> + 'a> {
match reader.spec().sample_format { match reader.spec().sample_format {
hound::SampleFormat::Float => Box::new(reader.samples::<f32>().map(|x| x.unwrap())), hound::SampleFormat::Float => Box::new(reader.samples::<f32>().map(|x| x.unwrap())),
hound::SampleFormat::Int => match reader.spec().bits_per_sample { hound::SampleFormat::Int => match reader.spec().bits_per_sample {
8 => Box::new( 8 => Box::new(reader.samples::<i8>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))),
reader 16 => Box::new(reader.samples::<i16>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))),
.samples::<i8>() 32 => Box::new(reader.samples::<i32>().map(|x| (x.unwrap() as f32) / (i32::max_value() as f32))),
.map(|x| (x.unwrap() as f32) / (i16::max_value() as f32)), _ => panic!("Unsupported sample rate")
), }
16 => Box::new(
reader
.samples::<i16>()
.map(|x| (x.unwrap() as f32) / (i16::max_value() as f32)),
),
32 => Box::new(
reader
.samples::<i32>()
.map(|x| (x.unwrap() as f32) / (i32::max_value() as f32)),
),
_ => panic!("Unsupported sample format"),
},
} }
} }