Initial commit
This commit is contained in:
commit
f1185e504e
8 changed files with 409 additions and 0 deletions
0
.envrc
Normal file
0
.envrc
Normal file
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
*.csv
|
||||
*.wav
|
||||
/*.html
|
||||
122
Cargo.lock
generated
Normal file
122
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
|
||||
[[package]]
|
||||
name = "i24"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bafc62750bce7c2603750d1c19532e226bb85bd21f5385ab952cc29b8c31e2af"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infraconv"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"readformat",
|
||||
"wavers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "readformat"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e1fd097dab477324dfeb476d75627e38fd1a7437bc0e94751084be499d7e6b1"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||
|
||||
[[package]]
|
||||
name = "wavers"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d599ddd98c95ad3d3fc898cfb5c4023430f6ea62f96083f46d13cc8b82589bd3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"i24",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"thiserror",
|
||||
]
|
||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "infraconv"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
readformat = "1.0.1"
|
||||
wavers = "1.5.1"
|
||||
48
README.md
Normal file
48
README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# infraconv
|
||||
|
||||
Infraconv converts accelerometer data to wav files for infrasound analysis.
|
||||
|
||||
## Usage
|
||||
|
||||
infraconv needs an input csv file.
|
||||
generate this csv by opening the html file accessible with `infraconv html` on your phone (it will be printed to stdout, you need to use a server like `python3 -m http.server 8080` to make use of it).
|
||||
|
||||
invocation: `infraconv <input csv file> <input samples/s> <output wav file> [<output params>]`
|
||||
|
||||
output params are any as follows:
|
||||
|
||||
- `speed <number>` speed multiplier: outputHz will be n * inputHz.
|
||||
- `multiply <number>` data multiplier: amplitude is multiplied by n. 5 is default.
|
||||
- `channel <x|y|z>` specifies which accelerometer channel to covert. z is default.
|
||||
- `integrate` sets integration mode, where the input is integrated first to enhance low frequencies that may not be picked up correctly.
|
||||
|
||||
when using integrate mode, multiply may need to be set to a lower number.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
infraconv html > accel.html
|
||||
python3 -m http.server 8080
|
||||
# open <ip>:8080 on phone and let it record for a few minutes
|
||||
# upload somewhere and download:
|
||||
wget "files.tudbut.de/accel(7).csv"
|
||||
# my phone does 128.6 Hz, for yours you'll have to read it from the screen
|
||||
infraconv "accel(7).csv" 128.6 out.wav speed 5 multiply 10
|
||||
mpv out.wav
|
||||
```
|
||||
|
||||
## Example output
|
||||
|
||||
```
|
||||
$ infraconv accel\(7\).csv 128.6 out.wav speed 5 multiply 10
|
||||
infraconv: Samples = 43920
|
||||
infraconv: Offset = 9.832353324890137
|
||||
infraconv: Out_Hz = 643
|
||||
infraconv: done
|
||||
```
|
||||
|
||||
## Input file layout
|
||||
|
||||
Input must be a csv. Channels are as follows: x,y,z. Having only x or x,y
|
||||
is possible, and any channel can be accessed. Thus, parsing data from
|
||||
other sources is possible.
|
||||
9
shell.nix
Normal file
9
shell.nix
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cargo
|
||||
helix
|
||||
rust-analyzer
|
||||
cargo-watch
|
||||
];
|
||||
}
|
||||
52
src/accel.html
Normal file
52
src/accel.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/nosleep/0.12.0/NoSleep.min.js"></script>
|
||||
<meta name="viewport" content="height=device-height width=device-width">
|
||||
|
||||
Recording. <span id="hz">0</span>Hz, <span id=n>0</span> samples collected.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<button type="button" onclick="download()">Download csv</button>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div id=samples></div>
|
||||
|
||||
<script>
|
||||
let noSleep = new NoSleep();
|
||||
noSleep.enable();
|
||||
let array = [];
|
||||
|
||||
let start_time = new Date().getTime();
|
||||
function handleMotionEvent(event) {
|
||||
const x = event.accelerationIncludingGravity.x;
|
||||
const y = event.accelerationIncludingGravity.y;
|
||||
const z = event.accelerationIncludingGravity.z;
|
||||
|
||||
array.push([x,y,z]);
|
||||
document.getElementById("hz").innerHTML = Math.round(1000 * array.length / ((new Date().getTime() - start_time) / 1000)) / 1000;
|
||||
document.getElementById("n").innerHTML = array.length;
|
||||
}
|
||||
|
||||
window.addEventListener("devicemotion", handleMotionEvent);
|
||||
|
||||
function download() {
|
||||
let data = array;
|
||||
let s = "";
|
||||
for(let entry of data) {
|
||||
s += entry[0] + "," + entry[1] + "," + entry[2] + "\n";
|
||||
}
|
||||
write_file(s, "accel.csv");
|
||||
}
|
||||
function write_file(data, name) {
|
||||
const blob = new Blob([data], {type:'text/csv'});
|
||||
let link = document.createElement("a");
|
||||
link.download = name;
|
||||
link.href = URL.createObjectURL(blob);
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove(link);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
166
src/main.rs
Normal file
166
src/main.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
use std::{env, fs};
|
||||
|
||||
use wavers::{Samples, write};
|
||||
|
||||
struct Params {
|
||||
speed: f32,
|
||||
channel: usize,
|
||||
multiply: f64,
|
||||
integrate: bool,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub fn new(args: &[String]) -> Self {
|
||||
let mut params = Params::default();
|
||||
let mut iter = args.iter();
|
||||
while let Some(key) = iter.next() {
|
||||
match key.as_str() {
|
||||
"speed" => {
|
||||
params.speed = iter
|
||||
.next()
|
||||
.expect("speed parameter expects a value.")
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
"channel" => {
|
||||
params.channel =
|
||||
match iter.next().expect("channel param expects a value").as_str() {
|
||||
"x" => 0,
|
||||
"y" => 1,
|
||||
"z" => 2,
|
||||
_ => panic!("invalid channel"),
|
||||
};
|
||||
}
|
||||
"multiply" => {
|
||||
params.multiply = iter
|
||||
.next()
|
||||
.expect("multiply parameter expects a value.")
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
"integrate" => {
|
||||
params.integrate = true;
|
||||
}
|
||||
x => panic!("Invalid parameter given {x}. Try --help"),
|
||||
}
|
||||
}
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Params {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
speed: 1.0,
|
||||
channel: 2,
|
||||
multiply: 5.0,
|
||||
integrate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
if args.get(1).is_some_and(|x| x == "html") {
|
||||
println!("{}", include_str!("accel.html"));
|
||||
return;
|
||||
}
|
||||
if args.len() < 4 || args.contains(&"--help".to_owned()) || args.contains(&"-h".to_owned()) {
|
||||
eprintln!(
|
||||
"\
|
||||
infraconv needs an input csv file.
|
||||
generate this csv by opening the html file accessible with `infraconv html` on your phone (it will be printed to stdout, you need to use a server like `python3 -m http.server 8080` to make use of it).
|
||||
|
||||
invocation: `infraconv <input csv file> <input samples/s> <output wav file> [<output params>]`
|
||||
|
||||
output params are any as follows:
|
||||
|
||||
- `speed <number>` speed multiplier: outputHz will be n * inputHz.
|
||||
- `multiply <number>` data multiplier: amplitude is multiplied by n. 5 is default.
|
||||
- `channel <x|y|z>` specifies which accelerometer channel to covert. z is default.
|
||||
- `integrate` sets integration mode, where the input is integrated first to enhance low frequencies that may not be picked up correctly.
|
||||
|
||||
when using integrate mode, multiply may need to be set to a lower number.\
|
||||
"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let csv_file = &args[1];
|
||||
let input_hz: f32 = args[2].parse().expect("expected a number for the Hz value");
|
||||
let output_file = &args[3];
|
||||
|
||||
let params = if args.len() > 4 {
|
||||
Params::new(&args[4..])
|
||||
} else {
|
||||
Params::default()
|
||||
};
|
||||
|
||||
let mut samples = read_csv(csv_file, params.channel);
|
||||
|
||||
println!("infraconv: Samples = {}", samples.len());
|
||||
|
||||
remove_offset(&mut samples);
|
||||
|
||||
if params.integrate {
|
||||
integrate(&mut samples, input_hz);
|
||||
remove_offset(&mut samples);
|
||||
}
|
||||
|
||||
multiply(&mut samples, params.multiply);
|
||||
|
||||
let samples = Samples::new(samples.into_boxed_slice());
|
||||
|
||||
let out_hz = (input_hz * params.speed).round() as i32;
|
||||
println!("infraconv: Out_Hz = {out_hz}");
|
||||
write(output_file, &samples.convert::<f64>(), out_hz, 1).unwrap();
|
||||
println!("infraconv: done");
|
||||
}
|
||||
|
||||
fn remove_offset(samples: &mut [f64]) {
|
||||
let mut offset = 0.0;
|
||||
let mut n = 0;
|
||||
for sample in samples.iter() {
|
||||
offset += sample;
|
||||
n += 1;
|
||||
}
|
||||
offset /= n as f64;
|
||||
|
||||
println!("infraconv: Offset = {offset}");
|
||||
|
||||
for sample in samples {
|
||||
*sample -= offset;
|
||||
}
|
||||
}
|
||||
|
||||
fn integrate(samples: &mut [f64], input_hz: f32) {
|
||||
let mut current = 0.0;
|
||||
for sample in samples {
|
||||
current += *sample;
|
||||
*sample = current;
|
||||
current -= current / (input_hz as f64 / 10.0);
|
||||
}
|
||||
|
||||
println!("infraconv: Integrated");
|
||||
}
|
||||
|
||||
fn multiply(samples: &mut Vec<f64>, multiply: f64) {
|
||||
for sample in samples {
|
||||
*sample *= multiply;
|
||||
}
|
||||
}
|
||||
|
||||
fn read_csv(file: &str, channel: usize) -> Vec<f64> {
|
||||
let csv = fs::read_to_string(file).expect("no existing input file provided");
|
||||
csv.lines()
|
||||
.filter(|line| line.contains(',') || !csv.contains(','))
|
||||
.map(|line| {
|
||||
let values = line.split(',');
|
||||
let values = values
|
||||
.into_iter()
|
||||
.map(|x| x.parse().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
values[channel]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue