Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
|
@ -1,18 +0,0 @@
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: audit-apt-decoder
|
|
||||||
- run: CARGO_HOME=/root/.cargo cargo audit
|
|
|
@ -1,55 +0,0 @@
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: build-apt-decoder
|
|
||||||
restore-keys: audit-apt-decoder
|
|
||||||
- run: CARGO_HOME=~/.cargo cargo build --release
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: build-apt-decoder
|
|
||||||
- run: CARGO_HOME=~/.cargo cargo build --target x86_64-pc-windows-gnu --release
|
|
||||||
|
|
||||||
build-appimage:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: build-apt-decoder
|
|
||||||
- run: CARGO_HOME=~/.cargo PATH=$PATH:$CARGO_HOME/bin x build -r --format appimage
|
|
|
@ -1,31 +0,0 @@
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-release:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: forgejo.zenerdio.de/sebastian/apt-decoder-ci:v0.2.1
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: build-apt-decoder
|
|
||||||
- run: CARGO_HOME=~/.cargo cargo build --target x86_64-pc-windows-gnu --release
|
|
||||||
- run: CARGO_HOME=~/.cargo PATH=$PATH:$CARGO_HOME/bin x build -r --format appimage
|
|
||||||
- run: mkdir -p release
|
|
||||||
- run: cp target/x/release/linux/x64/apt-decoder.AppImage release/apt-decoder-${{ github.ref_name }}.AppImage
|
|
||||||
- run: cp target/x86_64-pc-windows-gnu/release/apt-decoder.exe release/
|
|
||||||
- run: cd release && zip apt-decoder-win.zip apt-decoder.exe && rm apt-decoder.exe
|
|
||||||
- uses: actions/forgejo-release@v1
|
|
||||||
with:
|
|
||||||
direction: upload
|
|
||||||
release-dir: release
|
|
||||||
token: ${{ secrets.FORGEJO_RELEASE }}
|
|
|
@ -1,4 +1 @@
|
||||||
target
|
target
|
||||||
.vendor
|
|
||||||
*.AppImage
|
|
||||||
icon.png
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
|
@ -1,19 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "apt-decoder"
|
name = "apt-decoder"
|
||||||
version = "1.0.1"
|
version = "0.1.0"
|
||||||
authors = ["Sebastian <sebastian@sebastians-site.de>"]
|
authors = ["Sebastian <sebastian@sebastians-site.de>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = {version = "4.0.0", features = ["cargo"]}
|
hound = "*"
|
||||||
indicatif = "0.16.2"
|
image = "*"
|
||||||
hound = "3.4.0"
|
|
||||||
image = "0.24.0"
|
|
||||||
eframe = {version = "0.18.0", optional = true}
|
|
||||||
rfd = "0.11.4"
|
|
||||||
thiserror = "1.0.30"
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# Defines a feature named `webp` that does not enable any other features.
|
|
||||||
default = ["ui"]
|
|
||||||
ui = ["eframe"]
|
|
||||||
|
|
BIN
noaa19_short.png
BIN
noaa19_short.png
Binary file not shown.
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 420 KiB |
35
readme.md
35
readme.md
|
@ -9,45 +9,28 @@ apt-decoder provides a lightweight, simple to use and easy to understand solutio
|
||||||
|
|
||||||
![short sample](noaa19_short.png)
|
![short sample](noaa19_short.png)
|
||||||
|
|
||||||
Releases
|
|
||||||
--------
|
|
||||||
You can download the latest release as a standalone binary,
|
|
||||||
for linux (appimage) and windows (just a statically linked exe file) under
|
|
||||||
https://gitea.zenerdio.de/sebastian/apt-decoder/releases
|
|
||||||
|
|
||||||
Building
|
Building
|
||||||
--------
|
--------
|
||||||
1. Install the development packages for `libgtk3` and `libxcb` for your distro.
|
1. Install the rust compiler and cargo.
|
||||||
E.g. for anything Debian based:
|
E.g. using rustup (Try installing rustup using yourpackage manager,
|
||||||
`sudo apt install libgtk-3-dev libxcb-shape0-dev libxcb-xfixes0-dev`
|
don't use the stupid **curl | sh** stuff.)
|
||||||
2. Install the rust compiler and cargo.
|
2. Run `cargo build --release`
|
||||||
E.g. using rustup (Try installing rustup using your package manager,
|
3. The `apt-decoder` binary is in `target/release`
|
||||||
don't use the stupid `curl | sh` stuff.)
|
4. Done
|
||||||
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. To run `apt-decoder` in GUI mode just execute the binary.
|
2. Run `apt-decoder <your WAV file> <destination PNG file>`
|
||||||
For CLI mode use the `-n` flag:
|
You can also run the example contained in this repo:
|
||||||
`apt-decoder -n <your WAV file> <destination PNG file>`
|
`apt-decoder noaa19_short.wav noaa19_short.png`
|
||||||
For testing you can try the example contained in this repo:
|
|
||||||
`apt-decoder -n noaa19_short.wav noaa19_short.png`
|
|
||||||
3. Look at the generated PNG file, adjust the dynamic and contrast with your favorite tool.
|
3. Look at the generated PNG file, adjust the dynamic and contrast with your favorite tool.
|
||||||
4. Done
|
4. Done
|
||||||
|
|
||||||
![gui example](gui.png)
|
|
||||||
|
|
||||||
![long sample](example.png)
|
![long sample](example.png)
|
||||||
|
|
||||||
Theory of Operation
|
Theory of Operation
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
pub struct SquaringAMDemodulator<'a> {
|
pub struct SquaringAMDemodulator<'a> {
|
||||||
iterator: Box<dyn Iterator<Item = f32> + 'a>,
|
iterator: Box<Iterator<Item=f32> + 'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SquaringAMDemodulator<'a> {
|
impl<'a> SquaringAMDemodulator<'a> {
|
||||||
pub fn from<I>(iterator1: I) -> SquaringAMDemodulator<'a>
|
pub fn from<I>(iterator1: I) -> SquaringAMDemodulator<'a> where I: Iterator<Item=f32> + 'a {
|
||||||
where
|
|
||||||
I: Iterator<Item = f32> + 'a,
|
|
||||||
{
|
|
||||||
SquaringAMDemodulator {
|
SquaringAMDemodulator {
|
||||||
iterator: Box::new(iterator1),
|
iterator: Box::new(iterator1),
|
||||||
}
|
}
|
||||||
|
@ -19,7 +16,7 @@ impl<'a> Iterator for SquaringAMDemodulator<'a> {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
match self.iterator.next() {
|
match self.iterator.next() {
|
||||||
Some(x) => Some((x * x).sqrt()),
|
Some(x) => Some((x * x).sqrt()),
|
||||||
None => None,
|
None => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,42 @@
|
||||||
const SYNC_LENGHT: usize = 40;
|
const SYNC_LENGHT : usize = 40;
|
||||||
const SYNCA_SEQ: [bool; 40] = [
|
const SYNCA_SEQ : [bool; 40] = [false, false, false, false,
|
||||||
false, false, false, false, true, true, false, false, // Pulse 1
|
true, true, false, false, // Pulse 1
|
||||||
true, true, false, false, // Pulse 2
|
true, true, false, false, // Pulse 2
|
||||||
true, true, false, false, // Pulse 3
|
true, true, false, false, // Pulse 3
|
||||||
true, true, false, false, // Pulse 4
|
true, true, false, false, // Pulse 4
|
||||||
true, true, false, false, // Pulse 5
|
true, true, false, false, // Pulse 5
|
||||||
true, true, false, false, // Pulse 6
|
true, true, false, false, // Pulse 6
|
||||||
true, true, false, false, // Pulse 7
|
true, true, false, false, // Pulse 7
|
||||||
false, false, false, false, false, false, false, false,
|
false, false, false, false,
|
||||||
];
|
false, false, false, false,];
|
||||||
|
|
||||||
|
const SYNCB_SEQ : [bool; 40] = [false, false, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
true, true, true, false, false,
|
||||||
|
false];
|
||||||
|
|
||||||
const SYNCB_SEQ: [bool; 40] = [
|
|
||||||
false, false, false, false, true, true, true, false, false, true, true, true, false, false,
|
|
||||||
true, true, true, false, false, true, true, true, false, false, true, true, true, false, false,
|
|
||||||
true, true, true, false, false, true, true, true, false, false, false,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub enum SyncedSample {
|
pub enum SyncedSample {
|
||||||
Sample(f32),
|
Sample(f32),
|
||||||
SyncA(f32),
|
SyncA(f32),
|
||||||
SyncB(f32),
|
SyncB(f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct APTSyncer<'a> {
|
pub struct APTSyncer<'a> {
|
||||||
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<dyn Iterator<Item = f32> + 'a>,
|
iterator: Box<Iterator<Item=f32> + 'a>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> APTSyncer<'a> {
|
impl<'a> APTSyncer<'a> {
|
||||||
pub fn from<I>(mut iterator: I) -> APTSyncer<'a>
|
pub fn from<I>(mut iterator: I) -> APTSyncer<'a> where I: Iterator<Item=f32> + 'a {
|
||||||
where
|
|
||||||
I: Iterator<Item = f32> + 'a,
|
|
||||||
{
|
|
||||||
let mut state = [0.0; SYNC_LENGHT];
|
let mut state = [0.0; SYNC_LENGHT];
|
||||||
let mut avg_level = 0.5;
|
let mut avg_level = 0.5;
|
||||||
for i in 0..SYNC_LENGHT {
|
for i in 0..SYNC_LENGHT {
|
||||||
|
@ -42,17 +44,17 @@ impl<'a> APTSyncer<'a> {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
state[i] = x;
|
state[i] = x;
|
||||||
avg_level = 0.25 * x + avg_level * 0.75;
|
avg_level = 0.25 * x + avg_level * 0.75;
|
||||||
}
|
},
|
||||||
None => panic!("Could not retrieve enough samples to prime syncer"),
|
None => panic!("Could not retrieve enough samples to prime syncer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
APTSyncer {
|
APTSyncer {
|
||||||
state,
|
state: state,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
nones_read: 0,
|
nones_read: 0,
|
||||||
avg_level,
|
avg_level: avg_level,
|
||||||
iterator: Box::new(iterator),
|
iterator: Box::new(iterator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +87,7 @@ impl<'a> Iterator for APTSyncer<'a> {
|
||||||
type Item = SyncedSample;
|
type Item = SyncedSample;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
|
||||||
let (is_a, is_b) = self.is_marker();
|
let (is_a, is_b) = self.is_marker();
|
||||||
|
|
||||||
let sample = self.state[self.pos];
|
let sample = self.state[self.pos];
|
||||||
|
@ -92,8 +95,8 @@ impl<'a> Iterator for APTSyncer<'a> {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
self.state[self.pos] = x;
|
self.state[self.pos] = x;
|
||||||
self.avg_level = 0.25 * x + self.avg_level * 0.75;
|
self.avg_level = 0.25 * x + self.avg_level * 0.75;
|
||||||
}
|
},
|
||||||
None => self.nones_read += 1,
|
None => self.nones_read += 1
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.nones_read >= SYNC_LENGHT {
|
if self.nones_read >= SYNC_LENGHT {
|
||||||
|
@ -104,9 +107,11 @@ impl<'a> Iterator for APTSyncer<'a> {
|
||||||
|
|
||||||
if is_a {
|
if is_a {
|
||||||
return Some(SyncedSample::SyncA(sample));
|
return Some(SyncedSample::SyncA(sample));
|
||||||
} else if is_b {
|
}
|
||||||
|
else if is_b {
|
||||||
return Some(SyncedSample::SyncB(sample));
|
return Some(SyncedSample::SyncB(sample));
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return Some(SyncedSample::Sample(sample));
|
return Some(SyncedSample::Sample(sample));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
src/cli.rs
26
src/cli.rs
|
@ -1,26 +0,0 @@
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
|
||||||
|
|
||||||
use decoder;
|
|
||||||
|
|
||||||
const STEPS: u64 = 100;
|
|
||||||
|
|
||||||
pub fn decode(input_path: &str, output_path: &str) {
|
|
||||||
println!("Decoding {} to {}", input_path, output_path);
|
|
||||||
|
|
||||||
let bar = ProgressBar::new(STEPS).with_style(
|
|
||||||
ProgressStyle::default_bar()
|
|
||||||
.template("{spinner:.green} [{wide_bar}] {percent}% ({eta})")
|
|
||||||
.progress_chars("=> "),
|
|
||||||
);
|
|
||||||
let res = decoder::decode(input_path, output_path, |progress, _| {
|
|
||||||
bar.set_position((progress * STEPS as f32) as u64);
|
|
||||||
(true, STEPS as u32)
|
|
||||||
});
|
|
||||||
bar.finish();
|
|
||||||
|
|
||||||
if let Err(error) = res {
|
|
||||||
println!("Unable to decode file: {}", error);
|
|
||||||
} else {
|
|
||||||
println!("Done!")
|
|
||||||
}
|
|
||||||
}
|
|
197
src/decoder.rs
197
src/decoder.rs
|
@ -1,197 +0,0 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use amdemod::SquaringAMDemodulator;
|
|
||||||
use aptsyncer::{APTSyncer, SyncedSample};
|
|
||||||
use errors::DecoderError;
|
|
||||||
use firfilter::FIRFilter;
|
|
||||||
use resamplers::{Downsampler, Upsampler};
|
|
||||||
use utils::float_sample_iterator;
|
|
||||||
|
|
||||||
const LINES_PER_SECOND: u32 = 2;
|
|
||||||
const PIXELS_PER_LINE: u32 = 2080;
|
|
||||||
|
|
||||||
const LOWPASS_COEFFS: [f32; 63] = [
|
|
||||||
-7.383784e-03,
|
|
||||||
-3.183046e-03,
|
|
||||||
2.255039e-03,
|
|
||||||
7.461166e-03,
|
|
||||||
1.091908e-02,
|
|
||||||
1.149109e-02,
|
|
||||||
8.769802e-03,
|
|
||||||
3.252932e-03,
|
|
||||||
-3.720606e-03,
|
|
||||||
-1.027446e-02,
|
|
||||||
-1.447403e-02,
|
|
||||||
-1.486427e-02,
|
|
||||||
-1.092423e-02,
|
|
||||||
-3.307958e-03,
|
|
||||||
6.212477e-03,
|
|
||||||
1.511364e-02,
|
|
||||||
2.072873e-02,
|
|
||||||
2.096037e-02,
|
|
||||||
1.492345e-02,
|
|
||||||
3.347624e-03,
|
|
||||||
-1.138407e-02,
|
|
||||||
-2.560252e-02,
|
|
||||||
-3.507114e-02,
|
|
||||||
-3.591225e-02,
|
|
||||||
-2.553830e-02,
|
|
||||||
-3.371569e-03,
|
|
||||||
2.882645e-02,
|
|
||||||
6.711368e-02,
|
|
||||||
1.060042e-01,
|
|
||||||
1.394643e-01,
|
|
||||||
1.620650e-01,
|
|
||||||
1.700462e-01,
|
|
||||||
1.620650e-01,
|
|
||||||
1.394643e-01,
|
|
||||||
1.060042e-01,
|
|
||||||
6.711368e-02,
|
|
||||||
2.882645e-02,
|
|
||||||
-3.371569e-03,
|
|
||||||
-2.553830e-02,
|
|
||||||
-3.591225e-02,
|
|
||||||
-3.507114e-02,
|
|
||||||
-2.560252e-02,
|
|
||||||
-1.138407e-02,
|
|
||||||
3.347624e-03,
|
|
||||||
1.492345e-02,
|
|
||||||
2.096037e-02,
|
|
||||||
2.072873e-02,
|
|
||||||
1.511364e-02,
|
|
||||||
6.212477e-03,
|
|
||||||
-3.307958e-03,
|
|
||||||
-1.092423e-02,
|
|
||||||
-1.486427e-02,
|
|
||||||
-1.447403e-02,
|
|
||||||
-1.027446e-02,
|
|
||||||
-3.720606e-03,
|
|
||||||
3.252932e-03,
|
|
||||||
8.769802e-03,
|
|
||||||
1.149109e-02,
|
|
||||||
1.091908e-02,
|
|
||||||
7.461166e-03,
|
|
||||||
2.255039e-03,
|
|
||||||
-3.183046e-03,
|
|
||||||
-7.383784e-03,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn decode<T>(
|
|
||||||
input_file: &str,
|
|
||||||
output_file: &str,
|
|
||||||
progress_update: T,
|
|
||||||
) -> Result<(), DecoderError>
|
|
||||||
where
|
|
||||||
T: Fn(f32, image::RgbaImage) -> (bool, u32),
|
|
||||||
{
|
|
||||||
let mut reader =
|
|
||||||
hound::WavReader::open(input_file).map_err(|err| DecoderError::InputFileError(err))?;
|
|
||||||
|
|
||||||
if reader.spec().channels != 1 {
|
|
||||||
panic!("Expected a mono file");
|
|
||||||
}
|
|
||||||
|
|
||||||
let sample_rate = reader.spec().sample_rate;
|
|
||||||
if sample_rate != 48000 {
|
|
||||||
return Err(DecoderError::UnexpectedSamplingRate(sample_rate));
|
|
||||||
}
|
|
||||||
|
|
||||||
let sample_count = reader.len();
|
|
||||||
let seconds = (sample_count as f32) / (sample_rate as f32);
|
|
||||||
let lines = (seconds.ceil() as u32) * LINES_PER_SECOND;
|
|
||||||
|
|
||||||
let mut img = image::DynamicImage::ImageLuma8(image::ImageBuffer::new(PIXELS_PER_LINE, lines));
|
|
||||||
|
|
||||||
let coeffs = &LOWPASS_COEFFS;
|
|
||||||
|
|
||||||
let samples = float_sample_iterator(&mut reader);
|
|
||||||
|
|
||||||
let demod = SquaringAMDemodulator::from(samples);
|
|
||||||
let filter = FIRFilter::from(demod, coeffs);
|
|
||||||
let upsampler = Upsampler::from(filter, 13);
|
|
||||||
let downsampler = Downsampler::from(upsampler, 150);
|
|
||||||
let syncer = APTSyncer::from(downsampler);
|
|
||||||
|
|
||||||
let mut x = 0;
|
|
||||||
let mut y = 0;
|
|
||||||
let mut max_level = 0.0;
|
|
||||||
let mut has_sync = false;
|
|
||||||
|
|
||||||
let mut progress = 0;
|
|
||||||
let pixel_count = sample_count * 13 / 150;
|
|
||||||
let mut update_step = 10;
|
|
||||||
|
|
||||||
let mut previous_sample = 0.0;
|
|
||||||
|
|
||||||
for synced_sample in syncer {
|
|
||||||
progress += 1;
|
|
||||||
|
|
||||||
let sample = match synced_sample {
|
|
||||||
SyncedSample::Sample(s) => s,
|
|
||||||
SyncedSample::SyncA(s) => {
|
|
||||||
if !has_sync {
|
|
||||||
max_level = 0.0;
|
|
||||||
has_sync = true;
|
|
||||||
}
|
|
||||||
x = 0;
|
|
||||||
s
|
|
||||||
}
|
|
||||||
SyncedSample::SyncB(s) => {
|
|
||||||
if x < (PIXELS_PER_LINE / 2) {
|
|
||||||
let skip_distance = (PIXELS_PER_LINE / 2) - x;
|
|
||||||
let color = (previous_sample / max_level * 255.0) as u8;
|
|
||||||
for i in 0..skip_distance {
|
|
||||||
img.as_mut_luma8()
|
|
||||||
.unwrap()
|
|
||||||
.put_pixel(x + i, y, image::Luma([color]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !has_sync {
|
|
||||||
max_level = 0.0;
|
|
||||||
has_sync = true;
|
|
||||||
}
|
|
||||||
x = PIXELS_PER_LINE / 2;
|
|
||||||
s
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
max_level = f32::max(sample, max_level);
|
|
||||||
let color = (sample / max_level * 255.0) as u8;
|
|
||||||
|
|
||||||
if y < lines {
|
|
||||||
img.as_mut_luma8()
|
|
||||||
.unwrap()
|
|
||||||
.put_pixel(x, y, image::Luma([color]));
|
|
||||||
}
|
|
||||||
|
|
||||||
x += 1;
|
|
||||||
if x >= PIXELS_PER_LINE {
|
|
||||||
x = 0;
|
|
||||||
y += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
previous_sample = sample;
|
|
||||||
|
|
||||||
if progress % (PIXELS_PER_LINE * update_step) == 0 {
|
|
||||||
let (cont, update_steps) =
|
|
||||||
progress_update((progress as f32) / (pixel_count as f32), img.to_rgba8());
|
|
||||||
if !cont {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_count = pixel_count / PIXELS_PER_LINE;
|
|
||||||
update_step = if line_count / update_steps > 4 {
|
|
||||||
line_count / update_steps
|
|
||||||
} else {
|
|
||||||
4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress_update(1.0, img.to_rgba8());
|
|
||||||
|
|
||||||
img.save_with_format(&Path::new(output_file), image::ImageFormat::Png)
|
|
||||||
.map_err(|err| DecoderError::OutputFileError(err))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
use hound;
|
|
||||||
use image;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum DecoderError {
|
|
||||||
#[error("Unable to read input file: {0}")]
|
|
||||||
InputFileError(#[from] hound::Error),
|
|
||||||
|
|
||||||
#[error("Expected a sampling rate of 48000Hz not {0}Hz")]
|
|
||||||
UnexpectedSamplingRate(u32),
|
|
||||||
|
|
||||||
#[error("Unable to write output file: {0}")]
|
|
||||||
OutputFileError(#[from] image::ImageError),
|
|
||||||
}
|
|
|
@ -2,24 +2,21 @@ pub struct FIRFilter<'a> {
|
||||||
coeffs: &'a [f32],
|
coeffs: &'a [f32],
|
||||||
state: Vec<f32>,
|
state: Vec<f32>,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
iterator: Box<dyn Iterator<Item = f32> + 'a>,
|
iterator: Box<Iterator<Item=f32> + 'a>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FIRFilter<'a> {
|
impl<'a> FIRFilter<'a> {
|
||||||
pub fn from<I>(iterator: I, coeffs: &'a [f32]) -> FIRFilter<'a>
|
pub fn from<I>(iterator: I, coeffs: &'a [f32]) -> FIRFilter<'a> where I: Iterator<Item=f32> + 'a {
|
||||||
where
|
|
||||||
I: Iterator<Item = f32> + 'a,
|
|
||||||
{
|
|
||||||
let mut state = Vec::new();
|
let mut state = Vec::new();
|
||||||
for _ in 0..coeffs.len() {
|
for _ in 0..coeffs.len() {
|
||||||
state.push(0.0);
|
state.push(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
FIRFilter {
|
FIRFilter {
|
||||||
coeffs,
|
coeffs: coeffs,
|
||||||
state,
|
state: state,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
iterator: Box::new(iterator),
|
iterator: Box::new(iterator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +27,7 @@ impl<'a> Iterator for FIRFilter<'a> {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let cur = match self.iterator.next() {
|
let cur = match self.iterator.next() {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return None,
|
None => return None
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pos = (self.pos + 1) % self.coeffs.len();
|
self.pos = (self.pos + 1) % self.coeffs.len();
|
||||||
|
@ -40,7 +37,7 @@ impl<'a> Iterator for FIRFilter<'a> {
|
||||||
for i in 0..self.coeffs.len() {
|
for i in 0..self.coeffs.len() {
|
||||||
let pos = (self.pos + self.coeffs.len() - i) % self.coeffs.len();
|
let pos = (self.pos + self.coeffs.len() - i) % self.coeffs.len();
|
||||||
result += self.state[pos] * self.coeffs[i];
|
result += self.state[pos] * self.coeffs[i];
|
||||||
}
|
};
|
||||||
|
|
||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
|
|
207
src/main.rs
207
src/main.rs
|
@ -1,76 +1,153 @@
|
||||||
#![windows_subsystem = "windows"]
|
|
||||||
|
|
||||||
extern crate clap;
|
|
||||||
extern crate hound;
|
extern crate hound;
|
||||||
extern crate image;
|
extern crate image;
|
||||||
extern crate indicatif;
|
|
||||||
extern crate rfd;
|
|
||||||
extern crate thiserror;
|
|
||||||
|
|
||||||
#[cfg(feature = "ui")]
|
mod utils;
|
||||||
extern crate eframe;
|
|
||||||
|
|
||||||
mod amdemod;
|
|
||||||
mod aptsyncer;
|
|
||||||
mod cli;
|
|
||||||
mod decoder;
|
|
||||||
mod errors;
|
|
||||||
mod firfilter;
|
mod firfilter;
|
||||||
mod resamplers;
|
mod resamplers;
|
||||||
mod utils;
|
mod amdemod;
|
||||||
|
mod aptsyncer;
|
||||||
|
|
||||||
use clap::{arg, command};
|
use std::io::prelude::*;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use utils::float_sample_iterator;
|
||||||
|
use firfilter::FIRFilter;
|
||||||
|
use amdemod::SquaringAMDemodulator;
|
||||||
|
use resamplers::{Upsampler, Downsampler};
|
||||||
|
use aptsyncer::{APTSyncer, SyncedSample};
|
||||||
|
|
||||||
|
const LINES_PER_SECOND: u32 = 2;
|
||||||
|
const PIXELS_PER_LINE: u32 = 2080;
|
||||||
|
|
||||||
|
const LOWPASS_COEFFS : [f32; 63] = [ -7.383784e-03,
|
||||||
|
-3.183046e-03, 2.255039e-03, 7.461166e-03, 1.091908e-02,
|
||||||
|
1.149109e-02, 8.769802e-03, 3.252932e-03, -3.720606e-03,
|
||||||
|
-1.027446e-02, -1.447403e-02, -1.486427e-02, -1.092423e-02,
|
||||||
|
-3.307958e-03, 6.212477e-03, 1.511364e-02, 2.072873e-02,
|
||||||
|
2.096037e-02, 1.492345e-02, 3.347624e-03, -1.138407e-02,
|
||||||
|
-2.560252e-02, -3.507114e-02, -3.591225e-02, -2.553830e-02,
|
||||||
|
-3.371569e-03, 2.882645e-02, 6.711368e-02, 1.060042e-01,
|
||||||
|
1.394643e-01, 1.620650e-01, 1.700462e-01, 1.620650e-01,
|
||||||
|
1.394643e-01, 1.060042e-01, 6.711368e-02, 2.882645e-02,
|
||||||
|
-3.371569e-03, -2.553830e-02, -3.591225e-02, -3.507114e-02,
|
||||||
|
-2.560252e-02, -1.138407e-02, 3.347624e-03, 1.492345e-02,
|
||||||
|
2.096037e-02, 2.072873e-02, 1.511364e-02, 6.212477e-03,
|
||||||
|
-3.307958e-03, -1.092423e-02, -1.486427e-02, -1.447403e-02,
|
||||||
|
-1.027446e-02, -3.720606e-03, 3.252932e-03, 8.769802e-03,
|
||||||
|
1.149109e-02, 1.091908e-02, 7.461166e-03, 2.255039e-03,
|
||||||
|
-3.183046e-03, -7.383784e-03];
|
||||||
|
|
||||||
#[cfg(not(feature = "ui"))]
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = command!()
|
let args : Vec<String> = std::env::args().collect();
|
||||||
.arg(arg!([wavfile] "Input wav file with 48kHz samplingrate").required(true))
|
|
||||||
.arg(arg!([pngfile] "Output png file").default_value("output.png"))
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
let input_file = matches
|
if args.len() != 3 {
|
||||||
.get_one::<String>("wavfile")
|
println!("Usage: {} <wav file> <output file>", args[0]);
|
||||||
.expect("No input file given");
|
return;
|
||||||
|
|
||||||
let output_file = matches
|
|
||||||
.get_one::<String>("pngfile")
|
|
||||||
.expect("No output file given");
|
|
||||||
|
|
||||||
cli::decode(&input_file, &output_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ui")]
|
|
||||||
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))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut reader = match hound::WavReader::open(&args[1]) {
|
||||||
|
Err(e) => panic!("Could not open inputfile: {}", e),
|
||||||
|
Ok(r) => r
|
||||||
|
};
|
||||||
|
|
||||||
|
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 !");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
pub struct Upsampler<'a> {
|
pub struct Upsampler<'a> {
|
||||||
factor: u16,
|
factor: u16,
|
||||||
state: u16,
|
state: u16,
|
||||||
iterator: Box<dyn Iterator<Item = f32> + 'a>,
|
iterator: Box<Iterator<Item=f32> + 'a>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Upsampler<'a> {
|
impl<'a> Upsampler<'a> {
|
||||||
pub fn from<I>(iterator: I, factor: u16) -> Upsampler<'a>
|
pub fn from<I>(iterator: I, factor: u16) -> Upsampler<'a> where I: Iterator<Item=f32> + 'a {
|
||||||
where
|
|
||||||
I: Iterator<Item = f32> + 'a,
|
|
||||||
{
|
|
||||||
Upsampler {
|
Upsampler {
|
||||||
factor: factor,
|
factor: factor,
|
||||||
state: 0,
|
state: 0,
|
||||||
iterator: Box::new(iterator),
|
iterator: Box::new(iterator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +20,8 @@ impl<'a> Iterator for Upsampler<'a> {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let result = if self.state == 0 {
|
let result = if self.state == 0 {
|
||||||
self.iterator.next()
|
self.iterator.next()
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Some(0.0)
|
Some(0.0)
|
||||||
};
|
};
|
||||||
self.state = (self.state + 1) % self.factor;
|
self.state = (self.state + 1) % self.factor;
|
||||||
|
@ -32,19 +30,18 @@ impl<'a> Iterator for Upsampler<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct Downsampler<'a> {
|
pub struct Downsampler<'a> {
|
||||||
factor: u16,
|
factor: u16,
|
||||||
iterator: Box<dyn Iterator<Item = f32> + 'a>,
|
iterator: Box<Iterator<Item=f32> + 'a>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Downsampler<'a> {
|
impl<'a> Downsampler<'a> {
|
||||||
pub fn from<I>(iterator: I, factor: u16) -> Downsampler<'a>
|
pub fn from<I>(iterator: I, factor: u16) -> Downsampler<'a> where I: Iterator<Item=f32> + 'a {
|
||||||
where
|
|
||||||
I: Iterator<Item = f32> + 'a,
|
|
||||||
{
|
|
||||||
Downsampler {
|
Downsampler {
|
||||||
factor: factor,
|
factor: factor,
|
||||||
iterator: Box::new(iterator),
|
iterator: Box::new(iterator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +54,7 @@ impl<'a> Iterator for Downsampler<'a> {
|
||||||
for _ in 0..self.factor {
|
for _ in 0..self.factor {
|
||||||
match self.iterator.next() {
|
match self.iterator.next() {
|
||||||
Some(x) => result += x,
|
Some(x) => result += x,
|
||||||
None => return None,
|
None => return None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result /= self.factor as f32;
|
result /= self.factor as f32;
|
||||||
|
|
193
src/ui.rs
193
src/ui.rs
|
@ -1,193 +0,0 @@
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use eframe::egui;
|
|
||||||
use eframe::egui::text_edit::TextEdit;
|
|
||||||
use eframe::egui::widgets::{Button, ProgressBar};
|
|
||||||
use eframe::egui::ColorImage;
|
|
||||||
use eframe::egui::Visuals;
|
|
||||||
use eframe::egui::{Color32, RichText};
|
|
||||||
|
|
||||||
use decoder;
|
|
||||||
use errors::DecoderError;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum DecoderRunState {
|
|
||||||
RUNNING,
|
|
||||||
CANCELED,
|
|
||||||
DONE,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DecoderJobState {
|
|
||||||
update_steps: u32,
|
|
||||||
progress: f32,
|
|
||||||
texture: Option<egui::TextureHandle>,
|
|
||||||
run_state: DecoderRunState,
|
|
||||||
error: Option<DecoderError>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DecoderJobState {
|
|
||||||
fn is_running(&self) -> bool {
|
|
||||||
self.run_state == DecoderRunState::RUNNING
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DecoderJobState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
update_steps: 10,
|
|
||||||
progress: 0.0,
|
|
||||||
texture: None,
|
|
||||||
run_state: DecoderRunState::DONE,
|
|
||||||
error: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DecoderApp {
|
|
||||||
input_path: String,
|
|
||||||
output_path: String,
|
|
||||||
decoding_state: Arc<Mutex<DecoderJobState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DecoderApp {
|
|
||||||
pub fn new(input_path: &str, output_path: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
input_path: input_path.to_owned(),
|
|
||||||
output_path: output_path.to_owned(),
|
|
||||||
decoding_state: Arc::new(Mutex::new(DecoderJobState::default())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for DecoderApp {
|
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
|
||||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
let Self {
|
|
||||||
input_path,
|
|
||||||
output_path,
|
|
||||||
decoding_state,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut state = decoding_state.lock().unwrap();
|
|
||||||
|
|
||||||
if !ctx.input().raw.dropped_files.is_empty() && !state.is_running() {
|
|
||||||
if let Some(path) = ctx.input().raw.dropped_files[0].clone().path {
|
|
||||||
*input_path = path.display().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.set_visuals(Visuals::dark());
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
ui.heading("APT-Decoder");
|
|
||||||
|
|
||||||
egui::Grid::new("form_grid").num_columns(3).show(ui, |ui| {
|
|
||||||
ui.label("Input Wav File:");
|
|
||||||
ui.add_sized(
|
|
||||||
[300.0, 20.0],
|
|
||||||
TextEdit::singleline(input_path).interactive(!state.is_running()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.add_enabled(!state.is_running(), Button::new("Open"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
if let Some(path) = rfd::FileDialog::new().pick_file() {
|
|
||||||
*input_path = path.display().to_string();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("Output PNG File:");
|
|
||||||
ui.add_sized(
|
|
||||||
[300.0, 20.0],
|
|
||||||
TextEdit::singleline(output_path).interactive(!state.is_running()),
|
|
||||||
);
|
|
||||||
if ui
|
|
||||||
.add_enabled(!state.is_running(), Button::new("Save"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
if let Some(path) = rfd::FileDialog::new().save_file() {
|
|
||||||
*output_path = path.display().to_string();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ui.end_row();
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui
|
|
||||||
.add_enabled(!state.is_running(), Button::new("Decode"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
let decoding_state = decoding_state.clone();
|
|
||||||
let input_path = input_path.clone();
|
|
||||||
let output_path = output_path.clone();
|
|
||||||
|
|
||||||
state.error = None;
|
|
||||||
state.run_state = DecoderRunState::RUNNING;
|
|
||||||
state.texture = None;
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let decoder_res =
|
|
||||||
decoder::decode(&input_path, &output_path, |progress, image| {
|
|
||||||
let mut state = decoding_state.lock().unwrap();
|
|
||||||
|
|
||||||
state.progress = progress;
|
|
||||||
|
|
||||||
let size = [image.width() as _, image.height() as _];
|
|
||||||
let color_img = ColorImage::from_rgba_unmultiplied(
|
|
||||||
size,
|
|
||||||
image.as_flat_samples().as_slice(),
|
|
||||||
);
|
|
||||||
|
|
||||||
state.texture =
|
|
||||||
Some(ctx.load_texture("decoded-image", color_img));
|
|
||||||
|
|
||||||
ctx.request_repaint();
|
|
||||||
|
|
||||||
return (state.is_running(), state.update_steps);
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut state = decoding_state.lock().unwrap();
|
|
||||||
state.run_state = DecoderRunState::DONE;
|
|
||||||
state.error = match decoder_res {
|
|
||||||
Err(err) => Some(err),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.request_repaint();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add_enabled(
|
|
||||||
state.is_running(),
|
|
||||||
Button::new(RichText::new("Cancel").color(Color32::RED)),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
state.run_state = DecoderRunState::CANCELED;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let progressbar = ProgressBar::new(state.progress).show_percentage();
|
|
||||||
ui.add(progressbar);
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
if let Some(err) = &state.error {
|
|
||||||
ui.label(RichText::new(err.to_string()).color(Color32::RED));
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
let image_size = ui.available_size();
|
|
||||||
state.update_steps = image_size[1] as u32;
|
|
||||||
|
|
||||||
if let Some(texture) = &state.texture {
|
|
||||||
ui.image(texture, image_size);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
27
src/utils.rs
27
src/utils.rs
|
@ -4,28 +4,15 @@ extern crate hound;
|
||||||
|
|
||||||
type FileReader = std::io::BufReader<std::fs::File>;
|
type FileReader = std::io::BufReader<std::fs::File>;
|
||||||
|
|
||||||
pub fn float_sample_iterator<'a>(
|
pub fn float_sample_iterator<'a>(reader: &'a mut hound::WavReader<FileReader>)
|
||||||
reader: &'a mut hound::WavReader<FileReader>,
|
-> Box<Iterator<Item=f32> + 'a> {
|
||||||
) -> Box<dyn Iterator<Item = f32> + 'a> {
|
|
||||||
match reader.spec().sample_format {
|
match reader.spec().sample_format {
|
||||||
hound::SampleFormat::Float => Box::new(reader.samples::<f32>().map(|x| x.unwrap())),
|
hound::SampleFormat::Float => Box::new(reader.samples::<f32>().map(|x| x.unwrap())),
|
||||||
hound::SampleFormat::Int => match reader.spec().bits_per_sample {
|
hound::SampleFormat::Int => match reader.spec().bits_per_sample {
|
||||||
8 => Box::new(
|
8 => Box::new(reader.samples::<i8>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))),
|
||||||
reader
|
16 => Box::new(reader.samples::<i16>().map(|x| (x.unwrap() as f32) / (i16::max_value() as f32))),
|
||||||
.samples::<i8>()
|
32 => Box::new(reader.samples::<i32>().map(|x| (x.unwrap() as f32) / (i32::max_value() as f32))),
|
||||||
.map(|x| (x.unwrap() as f32) / (i16::max_value() as f32)),
|
_ => panic!("Unsupported sample rate")
|
||||||
),
|
}
|
||||||
16 => Box::new(
|
|
||||||
reader
|
|
||||||
.samples::<i16>()
|
|
||||||
.map(|x| (x.unwrap() as f32) / (i16::max_value() as f32)),
|
|
||||||
),
|
|
||||||
32 => Box::new(
|
|
||||||
reader
|
|
||||||
.samples::<i32>()
|
|
||||||
.map(|x| (x.unwrap() as f32) / (i32::max_value() as f32)),
|
|
||||||
),
|
|
||||||
_ => panic!("Unsupported sample format"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue