Compare commits
31 Commits
Author | SHA1 | Date |
---|---|---|
|
e18b23446c | |
|
5561fc32cb | |
|
00f020430f | |
|
c944f89ed3 | |
|
d3c66b6260 | |
|
6fbe5949e6 | |
|
d34d2ee2cc | |
|
84e7d85137 | |
|
166846c622 | |
|
a26ff950c1 | |
|
1f8249dead | |
|
81eda49f10 | |
|
b811bdc9ac | |
|
b9b971f1b0 | |
|
f44b58bb43 | |
|
dd133dfb20 | |
|
a5d7250e07 | |
|
6417820c9c | |
|
96a89f3091 | |
|
a07a522cf3 | |
|
16ffa5736f | |
|
d7e4d4eb5c | |
|
b095515238 | |
|
a5c4d930ac | |
|
bb56972ec8 | |
|
335dca9246 | |
|
2515e2e8f5 | |
|
0c9cf5ac24 | |
|
70a8b41578 | |
|
cc8afc1b07 | |
|
da0c5081e9 |
|
@ -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
|
|
@ -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
|
|
@ -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 }}
|
|
@ -1 +1,4 @@
|
||||||
target
|
target
|
||||||
|
.vendor
|
||||||
|
*.AppImage
|
||||||
|
icon.png
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
|
@ -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
noaa19_short.png
BIN
noaa19_short.png
Binary file not shown.
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 404 KiB |
35
readme.md
35
readme.md
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +1,58 @@
|
||||||
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 {
|
||||||
match iterator.next() {
|
match iterator.next() {
|
||||||
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,16 +85,15 @@ 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];
|
||||||
match self.iterator.next() {
|
match self.iterator.next() {
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
205
src/main.rs
205
src/main.rs
|
@ -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 !");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/utils.rs
27
src/utils.rs
|
@ -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"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue