Initial commit

This commit is contained in:
Tove 2025-10-25 22:17:49 +02:00
commit f1185e504e
8 changed files with 409 additions and 0 deletions

0
.envrc Normal file
View file

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
*.csv
*.wav
/*.html

122
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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()
}