Compare commits

...

31 Commits

Author SHA1 Message Date
Sebastian e18b23446c Updated some of the dependencies
/ publish-release (push) Successful in 4m32s Details
/ build-appimage (push) Successful in 2m54s Details
/ build (push) Successful in 2m52s Details
/ build-windows (push) Successful in 2m0s Details
/ audit (push) Successful in 2m17s Details
2023-08-02 20:31:11 +02:00
Sebastian 5561fc32cb Deleted woodpecker CI jobs
/ audit (push) Failing after 1m30s Details
/ build-windows (push) Successful in 1m57s Details
/ build (push) Successful in 46s Details
/ build-appimage (push) Successful in 3m21s Details
2023-08-02 20:04:06 +02:00
Sebastian 00f020430f Added release workflow 2023-08-02 20:03:58 +02:00
Sebastian c944f89ed3 Readded build-actions 2023-08-02 20:03:50 +02:00
Sebastian d3c66b6260 Recreated audit as forgejo action 2023-08-02 20:03:38 +02:00
Sebastian 6fbe5949e6 Copy cargo home between steps
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-02 15:22:44 +02:00
Sebastian d34d2ee2cc Merge remote-tracking branch 'origin/master' into develop
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-06-18 22:22:12 +02:00
Sebastian 84e7d85137 Speed up CI using prebuild image
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-06-18 20:54:22 +02:00
Sebastian 166846c622 Bumped version number
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-06-15 21:29:38 +02:00
Sebastian a26ff950c1 Updated eframe to fix cargo audit issues
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-06-15 20:51:33 +02:00
Sebastian 1f8249dead Bumped version in cargo.toml
ci/woodpecker/tag/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-04-10 23:18:32 +02:00
Sebastian 81eda49f10 Added releases page to readme
ci/woodpecker/push/woodpecker Pipeline is pending Details
2022-04-10 23:16:11 +02:00
Sebastian b811bdc9ac Added release stage to CI
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-04-10 23:13:20 +02:00
Sebastian b9b971f1b0 Updated readme
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-04-10 19:08:45 +02:00
Sebastian f44b58bb43 Added new CI targets
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-04-10 17:41:14 +02:00
Sebastian dd133dfb20 Removed namespace on optional dependency
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-04-10 15:38:16 +02:00
Sebastian a5d7250e07 Made the gui a feature
Added a nicer cli version
2022-04-10 15:33:51 +02:00
Sebastian 6417820c9c Fixed warnings
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-02-20 20:44:49 +01:00
Sebastian 96a89f3091 Added errors to ui
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-02-20 20:36:21 +01:00
Sebastian a07a522cf3 Fixed the app image extraction
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-02-13 21:57:30 +01:00
Sebastian 16ffa5736f CI fixes
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-02-13 19:50:03 +01:00
Sebastian d7e4d4eb5c Added windows annotation
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-02-13 19:36:25 +01:00
Sebastian b095515238 Update step needs a lower bound 2022-02-13 19:36:01 +01:00
Sebastian a5c4d930ac Added CI file
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-02-12 23:53:12 +01:00
Sebastian bb56972ec8 Adjusted update to image size 2022-02-12 23:00:52 +01:00
Sebastian 335dca9246 Use available UI size to size image 2022-02-09 23:01:59 +01:00
Sebastian 2515e2e8f5 Removed unecessarily cloned image 2022-02-09 22:52:32 +01:00
Sebastian 0c9cf5ac24 Use texture buffer to get the image across the thread barrier 2022-02-09 22:37:38 +01:00
Sebastian 70a8b41578 Fixed texture leak 2022-02-09 22:32:48 +01:00
Sebastian cc8afc1b07 First crude working versiom. Slow as crap 2022-02-09 21:28:26 +01:00
Sebastian da0c5081e9 Added a crude UI mockup 2022-02-09 17:41:04 +01:00
19 changed files with 3408 additions and 350 deletions

View File

@ -0,0 +1,18 @@
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

@ -0,0 +1,55 @@
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

@ -0,0 +1,31 @@
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 +1,4 @@
target
.vendor
*.AppImage
icon.png

2804
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,19 @@
[package]
name = "apt-decoder"
version = "0.1.0"
version = "1.0.1"
authors = ["Sebastian <sebastian@sebastians-site.de>"]
[dependencies]
hound = "*"
image = "*"
clap = {version = "4.0.0", features = ["cargo"]}
indicatif = "0.16.2"
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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 404 KiB

View File

@ -9,28 +9,45 @@ apt-decoder provides a lightweight, simple to use and easy to understand solutio
![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
--------
1. Install the rust compiler and cargo.
E.g. using rustup (Try installing rustup using yourpackage manager,
don't use the stupid **curl | sh** stuff.)
2. Run `cargo build --release`
3. The `apt-decoder` binary is in `target/release`
4. Done
1. Install the development packages for `libgtk3` and `libxcb` for your distro.
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 your package manager,
don't use the stupid `curl | sh` stuff.)
3. Run `cargo build --release`
4. The `apt-decoder` binary is in `target/release`
5. 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
-----
1. Save a received and FM-demodulated satellite signal as WAV-file.
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.
2. Run `apt-decoder <your WAV file> <destination PNG file>`
You can also run the example contained in this repo:
`apt-decoder noaa19_short.wav noaa19_short.png`
2. To run `apt-decoder` in GUI mode just execute the binary.
For CLI mode use the `-n` flag:
`apt-decoder -n <your WAV file> <destination PNG file>`
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.
4. Done
![gui example](gui.png)
![long sample](example.png)
Theory of Operation

View File

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

View File

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

26
src/cli.rs Normal file
View File

@ -0,0 +1,26 @@
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!")
}
}

197
src/decoder.rs Normal file
View File

@ -0,0 +1,197 @@
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(())
}

15
src/errors.rs Normal file
View File

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

View File

@ -1,153 +1,76 @@
#![windows_subsystem = "windows"]
extern crate clap;
extern crate hound;
extern crate image;
extern crate indicatif;
extern crate rfd;
extern crate thiserror;
#[cfg(feature = "ui")]
extern crate eframe;
mod utils;
mod firfilter;
mod resamplers;
mod amdemod;
mod aptsyncer;
mod cli;
mod decoder;
mod errors;
mod firfilter;
mod resamplers;
mod utils;
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];
use clap::{arg, command};
#[cfg(not(feature = "ui"))]
fn main() {
let args : Vec<String> = std::env::args().collect();
let matches = command!()
.arg(arg!([wavfile] "Input wav file with 48kHz samplingrate").required(true))
.arg(arg!([pngfile] "Output png file").default_value("output.png"))
.get_matches();
if args.len() != 3 {
println!("Usage: {} <wav file> <output file>", args[0]);
return;
}
let input_file = matches
.get_one::<String>("wavfile")
.expect("No input file given");
let mut reader = match hound::WavReader::open(&args[1]) {
Err(e) => panic!("Could not open inputfile: {}", e),
Ok(r) => r
};
let output_file = matches
.get_one::<String>("pngfile")
.expect("No output file given");
if reader.spec().channels != 1 {
panic!("Expected a mono file");
}
let sample_rate = reader.spec().sample_rate;
println!("Samplerate: {}", sample_rate);
if sample_rate != 48000 {
panic!("Expected a 48kHz 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;
println!("File contains {} seconds or {} lines", seconds, lines);
let mut img = 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 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 !");
cli::decode(&input_file, &output_file);
}
#[cfg(feature = "ui")]
mod ui;
#[cfg(feature = "ui")]
use ui::DecoderApp;
#[cfg(feature = "ui")]
fn main() {
let matches = command!()
.arg(arg!([wavfile] "Input wav file with 48kHz samplingrate").default_value("input.wav"))
.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
.get_one::<String>("wavfile")
.expect("No input file given")
.to_string();
let output_file = matches
.get_one::<String>("pngfile")
.expect("No output file given")
.to_string();
if matches.get_flag("nogui") {
cli::decode(&input_file, &output_file);
} else {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"APT-Decoder",
native_options,
Box::new(move |_cc| Box::new(DecoderApp::new(&input_file, &output_file))),
);
}
}

View File

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

193
src/ui.rs Normal file
View File

@ -0,0 +1,193 @@
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,15 +4,28 @@ extern crate hound;
type FileReader = std::io::BufReader<std::fs::File>;
pub fn float_sample_iterator<'a>(reader: &'a mut hound::WavReader<FileReader>)
-> Box<Iterator<Item=f32> + 'a> {
pub fn float_sample_iterator<'a>(
reader: &'a mut hound::WavReader<FileReader>,
) -> Box<dyn Iterator<Item = f32> + 'a> {
match reader.spec().sample_format {
hound::SampleFormat::Float => Box::new(reader.samples::<f32>().map(|x| x.unwrap())),
hound::SampleFormat::Int => match reader.spec().bits_per_sample {
8 => Box::new(reader.samples::<i8>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))),
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 rate")
}
8 => Box::new(
reader
.samples::<i8>()
.map(|x| (x.unwrap() as f32) / (i16::max_value() as f32)),
),
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"),
},
}
}