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 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] [package]
name = "apt-decoder" name = "apt-decoder"
version = "0.1.0" version = "1.0.1"
authors = ["Sebastian <sebastian@sebastians-site.de>"] authors = ["Sebastian <sebastian@sebastians-site.de>"]
[dependencies] [dependencies]
hound = "*" clap = {version = "4.0.0", features = ["cargo"]}
image = "*" 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) ![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 rust compiler and cargo. 1. Install the development packages for `libgtk3` and `libxcb` for your distro.
E.g. using rustup (Try installing rustup using yourpackage manager, E.g. for anything Debian based:
don't use the stupid **curl | sh** stuff.) `sudo apt install libgtk-3-dev libxcb-shape0-dev libxcb-xfixes0-dev`
2. Run `cargo build --release` 2. Install the rust compiler and cargo.
3. The `apt-decoder` binary is in `target/release` E.g. using rustup (Try installing rustup using your package manager,
4. Done 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 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. Run `apt-decoder <your WAV file> <destination PNG file>` 2. To run `apt-decoder` in GUI mode just execute the binary.
You can also run the example contained in this repo: For CLI mode use the `-n` flag:
`apt-decoder noaa19_short.wav noaa19_short.png` `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. 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,9 +1,12 @@
pub struct SquaringAMDemodulator<'a> { pub struct SquaringAMDemodulator<'a> {
iterator: Box<Iterator<Item=f32> + 'a>, iterator: Box<dyn Iterator<Item = f32> + 'a>,
} }
impl<'a> SquaringAMDemodulator<'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 { SquaringAMDemodulator {
iterator: Box::new(iterator1), iterator: Box::new(iterator1),
} }
@ -16,7 +19,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,42 +1,40 @@
const SYNC_LENGHT : usize = 40; const SYNC_LENGHT: usize = 40;
const SYNCA_SEQ : [bool; 40] = [false, false, false, false, const SYNCA_SEQ: [bool; 40] = [
true, true, false, false, // Pulse 1 false, false, false, false, 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> {
state: [f32; SYNC_LENGHT], state: [f32; SYNC_LENGHT],
pos : usize, pos: usize,
nones_read: usize, nones_read: usize,
avg_level : f32, avg_level: f32,
iterator: Box<Iterator<Item=f32> + 'a> iterator: Box<dyn Iterator<Item = f32> + 'a>,
} }
impl<'a> APTSyncer<'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 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 {
@ -44,17 +42,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),
} }
} }
@ -87,7 +85,6 @@ 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];
@ -95,8 +92,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 {
@ -107,11 +104,9 @@ 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));
} }
} }

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], coeffs: &'a [f32],
state: Vec<f32>, state: Vec<f32>,
pos: usize, pos: usize,
iterator: Box<Iterator<Item=f32> + 'a> iterator: Box<dyn Iterator<Item = f32> + 'a>,
} }
impl<'a> FIRFilter<'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(); 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),
} }
} }
} }
@ -27,7 +30,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();
@ -37,7 +40,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,153 +1,76 @@
#![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")]
extern crate eframe;
mod utils;
mod firfilter;
mod resamplers;
mod amdemod; mod amdemod;
mod aptsyncer; mod aptsyncer;
mod cli;
mod decoder;
mod errors;
mod firfilter;
mod resamplers;
mod utils;
use std::io::prelude::*; use clap::{arg, command};
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 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 { let input_file = matches
println!("Usage: {} <wav file> <output file>", args[0]); .get_one::<String>("wavfile")
return; .expect("No input file given");
}
let mut reader = match hound::WavReader::open(&args[1]) { let output_file = matches
Err(e) => panic!("Could not open inputfile: {}", e), .get_one::<String>("pngfile")
Ok(r) => r .expect("No output file given");
};
if reader.spec().channels != 1 { cli::decode(&input_file, &output_file);
panic!("Expected a mono file"); }
}
#[cfg(feature = "ui")]
let sample_rate = reader.spec().sample_rate; mod ui;
println!("Samplerate: {}", sample_rate);
if sample_rate != 48000 { #[cfg(feature = "ui")]
panic!("Expected a 48kHz sample rate"); use ui::DecoderApp;
}
#[cfg(feature = "ui")]
let sample_count = reader.len(); fn main() {
let seconds = (sample_count as f32) / (sample_rate as f32); let matches = command!()
let lines = (seconds.ceil() as u32) * LINES_PER_SECOND; .arg(arg!([wavfile] "Input wav file with 48kHz samplingrate").default_value("input.wav"))
println!("File contains {} seconds or {} lines", seconds, lines); .arg(arg!([pngfile] "Output png file").default_value("output.png"))
.arg(arg!(-n --nogui "Disable gui and run in command line mode"))
let mut img = image::ImageBuffer::new(PIXELS_PER_LINE, lines); .get_matches();
let coeffs = &LOWPASS_COEFFS; let input_file = matches
.get_one::<String>("wavfile")
let samples = float_sample_iterator(&mut reader); .expect("No input file given")
.to_string();
let demod = SquaringAMDemodulator::from(samples); let output_file = matches
let filter = FIRFilter::from(demod, coeffs); .get_one::<String>("pngfile")
let upsampler = Upsampler::from(filter, 13); .expect("No output file given")
let downsampler = Downsampler::from(upsampler, 150); .to_string();
let syncer = APTSyncer::from(downsampler);
if matches.get_flag("nogui") {
let mut x = 0; cli::decode(&input_file, &output_file);
let mut y = 0; } else {
let mut max_level = 0.0; let native_options = eframe::NativeOptions::default();
let mut has_sync = false;
eframe::run_native(
let mut progress = 0; "APT-Decoder",
let step = sample_count * 13 / 150 / 10; native_options,
Box::new(move |_cc| Box::new(DecoderApp::new(&input_file, &output_file))),
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,15 +1,18 @@
pub struct Upsampler<'a> { pub struct Upsampler<'a> {
factor: u16, factor: u16,
state: u16, state: u16,
iterator: Box<Iterator<Item=f32> + 'a> iterator: Box<dyn Iterator<Item = f32> + 'a>,
} }
impl<'a> Upsampler<'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 { Upsampler {
factor: factor, factor: factor,
state: 0, 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> { 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;
@ -30,18 +32,19 @@ impl<'a> Iterator for Upsampler<'a> {
} }
} }
pub struct Downsampler<'a> { pub struct Downsampler<'a> {
factor: u16, factor: u16,
iterator: Box<Iterator<Item=f32> + 'a> iterator: Box<dyn Iterator<Item = f32> + 'a>,
} }
impl<'a> Downsampler<'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 { Downsampler {
factor: factor, 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 { 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 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>; type FileReader = std::io::BufReader<std::fs::File>;
pub fn float_sample_iterator<'a>(reader: &'a mut hound::WavReader<FileReader>) pub fn float_sample_iterator<'a>(
-> Box<Iterator<Item=f32> + 'a> { reader: &'a mut hound::WavReader<FileReader>,
) -> 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(reader.samples::<i8>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))), 8 => Box::new(
16 => Box::new(reader.samples::<i16>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))), reader
32 => Box::new(reader.samples::<i32>().map(|x| (x.unwrap() as f32) / (i32::max_value() as f32))), .samples::<i8>()
_ => panic!("Unsupported sample rate") .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"),
},
} }
} }