Merge branch 'audio-logger'
This commit is contained in:
commit
3a2b209392
17
Makefile
Normal file
17
Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
|
||||
configure:
|
||||
|
||||
|
||||
build:
|
||||
|
||||
|
||||
run-audio-recorder:
|
||||
|
||||
|
||||
run-gps-recorder:
|
||||
|
||||
|
||||
run-depth-recorder:
|
||||
|
||||
|
31
README.md
31
README.md
@ -0,0 +1,31 @@
|
||||
# Hydrophonitor
|
||||
|
||||
A software package to record audio and related metadata from a configuration
|
||||
of hydrophones.
|
||||
|
||||
## Overview
|
||||
|
||||
Module | Description
|
||||
-----------------|-
|
||||
audio-logger | Receive an audio signal from the DAC and write it on disk in `.wav` format.
|
||||
gps-logger | Record position and time of the device in `.csv` format.
|
||||
depth-logger | Record depth of the device and save it in `.csv` format.
|
||||
*lcd-display | Provide information on the device's LCD screen
|
||||
*device-controls | Provide device control using physical buttons.
|
||||
|
||||
## Data Formats
|
||||
|
||||
Type | Output file format | Output file name | Output structure | Content
|
||||
------------|--------------------|--------------------------------------|------------------|
|
||||
Audio Data | .wav | <start-time-of-recording>_audio.wav | Each recorded chunk will be written to its own file in `audio` folder | Wav audio data, configuration defined in XXX
|
||||
GPS Data | .csv | <start-time-of-recording>_gps.wav | All data written to a single file | Csv data with following fields: GPS time UTC, latitude, longitude, speed, satellites in view
|
||||
Depth data | .csv | <start-time-of-recording>_depth.wav | All data written to a single file | Csv data with following fields: date and time, voltage of depth sensor (V), depth (m)
|
||||
Log data | .txt | <start-time-of-recording>_log.txt | All data written to a single file | Text file where each entry contains the following: date and time, process that writes the entry, logged information
|
||||
|
||||
## Output Locations
|
||||
|
||||
The base location/path for the output directories is defined by a configurable value BASE_DIR_PATH. If directories along this path do not exist, they will be created. If an error occurs or the location is not writable, output will be written to the default location (<x>) instead.
|
||||
|
||||
<ssd card automatic mount??>
|
||||
|
||||
A recording session starts when the Raspberry Pi is turned on or booted, and ends on shutdown. Each session will have its output written in its own directory that will be named <start-time-of-recording>_recordings.
|
3
audio-logger/.gitignore
vendored
Normal file
3
audio-logger/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
taget
|
||||
*.wav
|
||||
recordings
|
222
audio-logger/Cargo.lock
generated
222
audio-logger/Cargo.lock
generated
@ -51,20 +51,21 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "audio-logger"
|
||||
name = "audio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cpal",
|
||||
"cpal 0.13.5",
|
||||
"ctrlc",
|
||||
"hound",
|
||||
"jack 0.9.2",
|
||||
"libc",
|
||||
"nix 0.23.1",
|
||||
"parking_lot 0.12.1",
|
||||
"rodio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -104,6 +105,12 @@ version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.2.1"
|
||||
@ -205,6 +212,12 @@ dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "claxon"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.6"
|
||||
@ -255,7 +268,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mach",
|
||||
"ndk",
|
||||
"ndk 0.6.0",
|
||||
"ndk-glue",
|
||||
"nix 0.23.1",
|
||||
"oboe",
|
||||
@ -266,6 +279,31 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d466b47cf0ea4100186a7c12d7d0166813dda7cf648553554c9c39c6324841b"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach",
|
||||
"ndk 0.7.0",
|
||||
"ndk-context",
|
||||
"nix 0.23.1",
|
||||
"oboe",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.1",
|
||||
"stdweb",
|
||||
"thiserror",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.2.3"
|
||||
@ -276,6 +314,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cty"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
@ -523,6 +567,17 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lewton"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ogg",
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.133"
|
||||
@ -598,6 +653,26 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minimp3"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372"
|
||||
dependencies = [
|
||||
"minimp3-sys",
|
||||
"slice-deque",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimp3-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.6.0"
|
||||
@ -606,11 +681,25 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"jni-sys",
|
||||
"ndk-sys",
|
||||
"ndk-sys 0.3.0",
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"jni-sys",
|
||||
"ndk-sys 0.4.0",
|
||||
"num_enum",
|
||||
"raw-window-handle",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
@ -626,10 +715,10 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
"ndk 0.6.0",
|
||||
"ndk-context",
|
||||
"ndk-macro",
|
||||
"ndk-sys",
|
||||
"ndk-sys 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -654,6 +743,15 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21d83ec9c63ec5bf950200a8e508bdad6659972187b625469f58ef8c08e29046"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.23.1"
|
||||
@ -747,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk",
|
||||
"ndk 0.6.0",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
@ -763,6 +861,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ogg"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.15.0"
|
||||
@ -906,6 +1013,15 @@ dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a"
|
||||
dependencies = [
|
||||
"cty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@ -930,6 +1046,19 @@ version = "0.6.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
||||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb10b653d5ec0e9411a2e7d46e2c7f4046fd87d35b9955bd73ba4108d69072b5"
|
||||
dependencies = [
|
||||
"claxon",
|
||||
"cpal 0.14.0",
|
||||
"hound",
|
||||
"lewton",
|
||||
"minimp3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@ -963,6 +1092,17 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
||||
|
||||
[[package]]
|
||||
name = "slice-deque"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mach",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.9.0"
|
||||
@ -1049,6 +1189,21 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
@ -1188,17 +1343,30 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc 0.37.0",
|
||||
"windows_i686_gnu 0.37.0",
|
||||
"windows_i686_msvc 0.37.0",
|
||||
"windows_x86_64_gnu 0.37.0",
|
||||
"windows_x86_64_msvc 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_msvc 0.36.1",
|
||||
"windows_i686_gnu 0.36.1",
|
||||
"windows_i686_msvc 0.36.1",
|
||||
"windows_x86_64_gnu 0.36.1",
|
||||
"windows_x86_64_msvc 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1207,26 +1375,56 @@ version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
|
||||
|
@ -1,17 +1,16 @@
|
||||
[package]
|
||||
name = "audio-logger"
|
||||
name = "audio"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = {url = "https://github.com/clap-rs/clap", features = ["derive"]}
|
||||
clap = { url = "https://github.com/clap-rs/clap", features = ["derive"] }
|
||||
cpal= { version = "0.13.5", features = ["jack"] }
|
||||
anyhow = "1.0.61"
|
||||
hound = "3.4.0"
|
||||
chrono = "0.4.22"
|
||||
ctrlc = "3.2.3"
|
||||
rodio = "0.16.0"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies]
|
||||
alsa = "0.6"
|
||||
|
@ -1,9 +1,9 @@
|
||||
test: all
|
||||
bash run_test.sh
|
||||
|
||||
all:
|
||||
cargo build --release
|
||||
|
||||
test: all
|
||||
mkdir -p recordings
|
||||
bash run_test.sh
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
@ -11,7 +11,6 @@ clean:
|
||||
fclean: clean
|
||||
rm -rf recordings
|
||||
|
||||
re: fclean
|
||||
cargo build --release
|
||||
re: fclean all
|
||||
|
||||
.PHONY: all clean re test
|
||||
.PHONY: all clean fclean re test
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
target/release/audio-logger \
|
||||
target/release/audio rec \
|
||||
--name test \
|
||||
--output recordings/ \
|
||||
--batch-recording 3 \
|
||||
|
@ -1,14 +1,46 @@
|
||||
use clap::{Parser, ValueEnum};
|
||||
use clap::{Parser, ValueEnum, Subcommand};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum Hosts {
|
||||
Alsa,
|
||||
Jack,
|
||||
CoreAudio,
|
||||
Asio,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[clap(about = "A tool to record audio on Linux using the command line.")]
|
||||
pub enum Commands {
|
||||
/// Record audio either continously or in batches (in case a possible
|
||||
/// interruption is an issue)
|
||||
Rec(Rec),
|
||||
/// Play audio from a .wav file
|
||||
Play(Play),
|
||||
/// Merge audio resulting from a batch recording into a single file
|
||||
Merge(Merge),
|
||||
/// Get reports of system's audio resources
|
||||
Info(Info),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about = "A tool to record audio.")]
|
||||
pub struct Args {
|
||||
#[clap(about = "Merge audio resulting from a batch recording into a single
|
||||
file.
|
||||
")]
|
||||
pub struct Merge {
|
||||
/// The directory to look for files to merge.
|
||||
#[clap(short, long, default_value = "./", required = true)]
|
||||
pub input: std::path::PathBuf,
|
||||
|
||||
/// The directory to save the merged file to.
|
||||
#[clap(short, long, default_value = "./")]
|
||||
pub output: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about = "Record audio either continously or in batches (in case a
|
||||
possible interruption is an issue).
|
||||
")]
|
||||
pub struct Rec {
|
||||
|
||||
/// Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav`
|
||||
#[clap(required = true, short, long)]
|
||||
@ -22,9 +54,9 @@ pub struct Args {
|
||||
#[clap(short, long, value_name = "SECONDS")]
|
||||
pub batch_recording: Option<u64>,
|
||||
|
||||
/// Output the available devices and their configurations
|
||||
#[clap(long)]
|
||||
pub print_configs: bool,
|
||||
/// (optional) Will record for a total of [SECONDS]
|
||||
#[clap(short, long, value_name = "MAX_SECONDS")]
|
||||
pub max_seconds: Option<u64>,
|
||||
|
||||
/// Host API to use
|
||||
#[clap(value_enum)]
|
||||
@ -42,3 +74,27 @@ pub struct Args {
|
||||
#[clap(long, value_name = "FRAMES")]
|
||||
pub buffer_size: Option<u32>,
|
||||
}
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about = "Play audio from a .wav file.
|
||||
")]
|
||||
pub struct Play {
|
||||
/// Path to the file to play
|
||||
#[clap(value_parser, value_name = "PATH", value_hint = clap::ValueHint::FilePath)]
|
||||
pub input: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about = "Get reports of system's audio resources.")]
|
||||
pub struct Info {
|
||||
/// Output the available devices and their configurations
|
||||
#[clap(long)]
|
||||
pub print_configs: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
7
audio-logger/src/constants.rs
Normal file
7
audio-logger/src/constants.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub const DEFAULT_SAMPLE_RATE: u32 = 44100;
|
||||
pub const DEFAULT_CHANNEL_COUNT: u16 = 1;
|
||||
pub const DEFAULT_BUFFER_SIZE: u32 = 1024;
|
||||
pub const ALLOWED_SAMPLE_RATES: [u32; 6] = [44100, 48000, 88200, 96000, 176400, 192000];
|
||||
pub const MAX_CHANNEL_COUNT: u16 = 2;
|
||||
pub const MIN_BUFFER_SIZE: usize = 64;
|
||||
pub const MAX_BUFFER_SIZE: usize = 8192;
|
113
audio-logger/src/getters.rs
Normal file
113
audio-logger/src/getters.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use super::*;
|
||||
use cpal::{
|
||||
StreamConfig,
|
||||
SupportedStreamConfig,
|
||||
Device,
|
||||
HostId,
|
||||
Host,
|
||||
SampleRate,
|
||||
BufferSize,
|
||||
traits::{DeviceTrait, HostTrait},
|
||||
};
|
||||
use hound::WavSpec;
|
||||
use std::path::PathBuf;
|
||||
use chrono::*;
|
||||
use anyhow::{Error, Result, anyhow};
|
||||
|
||||
/// # Get Host
|
||||
///
|
||||
/// Returns the host with the given id if it's available.
|
||||
pub fn get_host(host: HostId) -> Result<Host, Error> {
|
||||
Ok(cpal::host_from_id(cpal::available_hosts()
|
||||
.into_iter()
|
||||
.find(|id| *id == host)
|
||||
.ok_or(anyhow!("Requested host device not found"))?
|
||||
)?)
|
||||
}
|
||||
|
||||
/// # Get Device
|
||||
///
|
||||
/// Returns the default input device for the host if it's available.
|
||||
pub fn get_device(host: Host) -> Result<Device, Error> {
|
||||
Ok(host.default_input_device()
|
||||
.ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`",
|
||||
))?)
|
||||
}
|
||||
|
||||
/// # Get Default Config
|
||||
///
|
||||
/// Get the default config for the given device.
|
||||
pub fn get_default_config(device: &Device) -> Result<SupportedStreamConfig, Error> {
|
||||
Ok(device.default_input_config()?)
|
||||
}
|
||||
|
||||
/// # Get User Config
|
||||
///
|
||||
/// Overrides certain fields of the default stream config with the user's config.
|
||||
///
|
||||
/// sample_rate: The user's sample rate if it is supported by the device, otherwise the default sample rate.
|
||||
/// channels: The user's number of channels if it is supported by the device, otherwise the default number of channels.
|
||||
/// buffer_size: The user's buffer size if it is supported by the device, otherwise the default buffer size.
|
||||
pub fn get_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Result<StreamConfig, Error> {
|
||||
if !ALLOWED_SAMPLE_RATES.contains(&sample_rate) {
|
||||
return Err(anyhow!(
|
||||
"Sample rate {} is not supported. Allowed sample rates: {:?}",
|
||||
sample_rate,
|
||||
ALLOWED_SAMPLE_RATES
|
||||
));
|
||||
}
|
||||
if !(channels >= 1 && channels <= MAX_CHANNEL_COUNT) {
|
||||
return Err(anyhow!(
|
||||
"Channel count {} is not supported. Allowed channel counts: 1-{}",
|
||||
channels,
|
||||
MAX_CHANNEL_COUNT
|
||||
));
|
||||
}
|
||||
if !(buffer_size >= MIN_BUFFER_SIZE as u32 && buffer_size <= MAX_BUFFER_SIZE as u32) {
|
||||
return Err(anyhow!(
|
||||
"Buffer size {} is not supported. Allowed buffer sizes: {}-{}",
|
||||
buffer_size,
|
||||
MIN_BUFFER_SIZE,
|
||||
MAX_BUFFER_SIZE
|
||||
));
|
||||
}
|
||||
Ok(StreamConfig {
|
||||
channels,
|
||||
sample_rate: SampleRate(sample_rate),
|
||||
buffer_size: BufferSize::Fixed(buffer_size),
|
||||
})
|
||||
}
|
||||
|
||||
/// # Get WAV Spec
|
||||
///
|
||||
/// Get the WAV spec for the given stream config.
|
||||
pub fn get_wav_spec(default_config: &SupportedStreamConfig, user_config: &StreamConfig) -> Result<WavSpec, Error> {
|
||||
Ok(WavSpec {
|
||||
channels: user_config.channels,
|
||||
sample_rate: user_config.sample_rate.0,
|
||||
bits_per_sample: (default_config.sample_format().sample_size() * 8) as u16,
|
||||
sample_format: match default_config.sample_format() {
|
||||
cpal::SampleFormat::U16 => hound::SampleFormat::Int,
|
||||
cpal::SampleFormat::I16 => hound::SampleFormat::Int,
|
||||
cpal::SampleFormat::F32 => hound::SampleFormat::Float,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_date_time_string() -> String {
|
||||
let now: DateTime<Local> = Local::now();
|
||||
format!(
|
||||
"{}-{}-{}_{}:{}:{:02}",
|
||||
now.year(), now.month(), now.day(),
|
||||
now.hour(), now.minute(), now.second(),
|
||||
)
|
||||
}
|
||||
/// # Get Filename
|
||||
///
|
||||
/// Get the filename for the current recording according to the given format,
|
||||
/// the current date and time, and the name prefix.
|
||||
pub fn get_filename(name: &str, path: &PathBuf) -> String {
|
||||
let mut filename = path.clone();
|
||||
filename.push(format!("{}_{}.wav", get_date_time_string(), name));
|
||||
filename.to_str().unwrap().to_string()
|
||||
}
|
70
audio-logger/src/input_handling.rs
Normal file
70
audio-logger/src/input_handling.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use super::*;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
Mutex,
|
||||
Condvar,
|
||||
atomic::{AtomicBool, Ordering}
|
||||
};
|
||||
|
||||
type StreamInterrupt = Arc<(Mutex<bool>, Condvar)>;
|
||||
type BatchInterrupt= Arc<AtomicBool>;
|
||||
|
||||
/// # Interrupts Handling
|
||||
///
|
||||
/// The `Recorder` struct has two interrupt mechanisms:
|
||||
///
|
||||
/// 1. `stream_interrupt` is used to interrupt the stream when the user presses `ctrl+c`.
|
||||
/// 2. `batch_interrupt` is used to interrupt the batch recording when the user presses `ctrl+c`.
|
||||
#[derive(Clone)]
|
||||
pub struct InterruptHandles {
|
||||
batch_interrupt: BatchInterrupt,
|
||||
stream_interrupt: StreamInterrupt,
|
||||
max_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
impl InterruptHandles {
|
||||
pub fn new(max_seconds: Option<u64>) -> Result<Self, anyhow::Error> {
|
||||
let stream_interrupt = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let stream_interrupt_cloned = stream_interrupt.clone();
|
||||
|
||||
let batch_interrupt = Arc::new(AtomicBool::new(false));
|
||||
let batch_interrupt_cloned = batch_interrupt.clone();
|
||||
|
||||
ctrlc::set_handler(move || {
|
||||
// Set batch interrupt to true
|
||||
batch_interrupt_cloned.store(true, Ordering::SeqCst);
|
||||
|
||||
// Release the stream
|
||||
let &(ref lock, ref cvar) = &*stream_interrupt_cloned;
|
||||
let mut started = lock.lock().unwrap();
|
||||
*started = true;
|
||||
cvar.notify_one();
|
||||
})?;
|
||||
Ok(Self {
|
||||
batch_interrupt,
|
||||
stream_interrupt,
|
||||
max_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn stream_wait(&self) {
|
||||
let &(ref lock, ref cvar) = &*self.stream_interrupt;
|
||||
let mut started = lock.lock().unwrap();
|
||||
let now = std::time::Instant::now();
|
||||
while !*started {
|
||||
match self.max_seconds {
|
||||
Some(secs) => {
|
||||
if now.elapsed().as_secs() >= secs {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
started = cvar.wait(started).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn batch_is_running(&self) -> bool {
|
||||
!self.batch_interrupt.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
@ -1,97 +1,43 @@
|
||||
//! Records a WAV file (roughly 3 seconds long) using the default input device and config.
|
||||
//!
|
||||
//! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav".
|
||||
//! # Main
|
||||
mod cli;
|
||||
mod recorder;
|
||||
mod print_configs;
|
||||
mod getters;
|
||||
mod input_handling;
|
||||
mod merge;
|
||||
mod constants;
|
||||
mod play;
|
||||
|
||||
use print_configs::*;
|
||||
use cli::*;
|
||||
use merge::*;
|
||||
use recorder::*;
|
||||
use constants::*;
|
||||
use getters::*;
|
||||
use input_handling::*;
|
||||
use play::*;
|
||||
use anyhow::{Error, Result, anyhow};
|
||||
|
||||
use clap::Parser;
|
||||
use recorder::{batch_recording, contiguous_recording};
|
||||
use cli::*;
|
||||
use std::path::Path;
|
||||
use print_configs::*;
|
||||
use std::sync::{Arc, Mutex, Condvar, atomic::{AtomicBool, Ordering}};
|
||||
|
||||
const DEFAULT_SAMPLE_RATE: u32 = 44100;
|
||||
const DEFAULT_CHANNEL_COUNT: u16 = 1;
|
||||
const DEFAULT_BUFFER_SIZE: u32 = 1024;
|
||||
const ALLOWED_SAMPLE_RATES: &[u32] = &[44100, 48000, 88200, 96000, 176400, 192000];
|
||||
const MAX_CHANNEL_COUNT: u16 = 2;
|
||||
const MIN_BUFFER_SIZE: usize = 64;
|
||||
const MAX_BUFFER_SIZE: usize = 8192;
|
||||
const FMT_TIME: &str = "%Y-%m-%d-%H:%M:%S.%f";
|
||||
fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
type StreamInterrupt = Arc<(Mutex<bool>, Condvar)>;
|
||||
type BatchInterrupt= Arc<AtomicBool>;
|
||||
|
||||
/// # Interrupts Handling
|
||||
///
|
||||
/// The `Recorder` struct has two interrupt mechanisms:
|
||||
///
|
||||
/// 1. `stream_interrupt` is used to interrupt the stream when the user presses `ctrl+c`.
|
||||
/// 2. `batch_interrupt` is used to interrupt the batch recording when the user presses `ctrl+c`.
|
||||
#[derive(Clone)]
|
||||
pub struct InterruptHandles {
|
||||
batch_interrupt: BatchInterrupt,
|
||||
stream_interrupt: StreamInterrupt,
|
||||
}
|
||||
|
||||
impl InterruptHandles {
|
||||
pub fn new() -> Result<Self, anyhow::Error> {
|
||||
let stream_interrupt = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let stream_interrupt_cloned = stream_interrupt.clone();
|
||||
|
||||
let batch_interrupt = Arc::new(AtomicBool::new(false));
|
||||
let batch_interrupt_cloned = batch_interrupt.clone();
|
||||
|
||||
ctrlc::set_handler(move || {
|
||||
// Set batch interrupt to true
|
||||
batch_interrupt_cloned.store(true, Ordering::SeqCst);
|
||||
|
||||
// Release the stream
|
||||
let &(ref lock, ref cvar) = &*stream_interrupt_cloned;
|
||||
let mut started = lock.lock().unwrap();
|
||||
*started = true;
|
||||
cvar.notify_one();
|
||||
})?;
|
||||
Ok(Self {
|
||||
batch_interrupt,
|
||||
stream_interrupt,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn stream_wait(&self) {
|
||||
let &(ref lock, ref cvar) = &*self.stream_interrupt;
|
||||
let mut started = lock.lock().unwrap();
|
||||
while !*started {
|
||||
started = cvar.wait(started).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn batch_is_running(&self) -> bool {
|
||||
!self.batch_interrupt.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.print_configs {
|
||||
print_configs()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let interrupt_handles = InterruptHandles::new()?;
|
||||
|
||||
match args.batch_recording {
|
||||
Some(secs) => {
|
||||
batch_recording(&args, secs, interrupt_handles)?;
|
||||
match &cli.command {
|
||||
Commands::Rec(args) => {
|
||||
record(&args)?;
|
||||
},
|
||||
None => {
|
||||
contiguous_recording(&args, interrupt_handles)?;
|
||||
Commands::Play(args) => {
|
||||
play(&args.input)?;
|
||||
},
|
||||
Commands::Info(args) => {
|
||||
if args.print_configs {
|
||||
print_configs()?;
|
||||
}
|
||||
},
|
||||
Commands::Merge(args) => {
|
||||
merge_wavs(&args.input, &args.output)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
43
audio-logger/src/merge.rs
Normal file
43
audio-logger/src/merge.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use hound::{WavReader, WavWriter};
|
||||
use anyhow::{Result, Error};
|
||||
|
||||
pub fn merge_wavs(input: &std::path::PathBuf, output: &std::path::PathBuf) -> Result<(), Error> {
|
||||
// Read files from input directory
|
||||
let mut files = std::fs::read_dir(input)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.file_type().ok().map(|t| t.is_file()).unwrap_or(false))
|
||||
.filter(|entry| entry.path().extension().unwrap_or_default() == "wav")
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort files by name
|
||||
files.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
|
||||
|
||||
// Use the name of the first file as the name of the output file
|
||||
let output_name = files.first().unwrap().file_name();
|
||||
let output_name = output_name.to_str().unwrap();
|
||||
|
||||
// Get wav spec from file
|
||||
let spec = WavReader::open(files.first().unwrap().path())?.spec();
|
||||
let mut writer = WavWriter::create(output.join(output_name), spec)?;
|
||||
|
||||
match spec.sample_format {
|
||||
hound::SampleFormat::Float => {
|
||||
for file in files {
|
||||
let mut reader = WavReader::open(file.path())?;
|
||||
for sample in reader.samples::<f32>() {
|
||||
writer.write_sample(sample?)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
hound::SampleFormat::Int => {
|
||||
for file in files {
|
||||
let mut reader = WavReader::open(file.path())?;
|
||||
for sample in reader.samples::<i32>() {
|
||||
writer.write_sample(sample?)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
writer.finalize()?;
|
||||
Ok(())
|
||||
}
|
14
audio-logger/src/play.rs
Normal file
14
audio-logger/src/play.rs
Normal file
@ -0,0 +1,14 @@
|
||||
//! Play audio from a .wav file.
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use rodio::{Decoder, OutputStream, Sink};
|
||||
|
||||
pub fn play(path: &std::path::PathBuf) -> Result<(), Error> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let source = Decoder::new(file).unwrap();
|
||||
let (_stream, stream_handle) = OutputStream::try_default()?;
|
||||
let sink = Sink::try_new(&stream_handle)?;
|
||||
sink.append(source);
|
||||
sink.sleep_until_end();
|
||||
Ok(())
|
||||
}
|
@ -1,5 +1,19 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
|
||||
// pub fn print_available_hosts() -> Result<(), anyhow::Error> {
|
||||
// let hosts = cpal::available_hosts();
|
||||
// println!("Available hosts:");
|
||||
// for host in hosts {
|
||||
// println!(" {}", host.name());
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// pub fn print_supported_hosts() {
|
||||
// println!("Supported hosts:");
|
||||
// println!("{:?}", cpal::ALL_HOSTS);
|
||||
// }
|
||||
|
||||
pub fn print_configs() -> Result<(), anyhow::Error> {
|
||||
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
|
||||
let available_hosts = cpal::available_hosts();
|
||||
|
@ -1,67 +1,49 @@
|
||||
use anyhow::anyhow;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::*;
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use chrono::prelude::*;
|
||||
use super::*;
|
||||
use cpal::{
|
||||
StreamConfig,
|
||||
SupportedStreamConfig,
|
||||
Device,
|
||||
HostId,
|
||||
Stream,
|
||||
traits::{DeviceTrait, StreamTrait},
|
||||
};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::BufWriter,
|
||||
path::{PathBuf, Path},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use anyhow::Error;
|
||||
|
||||
type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
|
||||
|
||||
pub struct Recorder {
|
||||
writer: WriteHandle,
|
||||
interrupt: InterruptHandles,
|
||||
interrupt_handles: InterruptHandles,
|
||||
default_config: SupportedStreamConfig,
|
||||
user_config: StreamConfig,
|
||||
device: Device,
|
||||
filename: String,
|
||||
}
|
||||
|
||||
/// # Stream User Config
|
||||
///
|
||||
/// Overrides certain fields of the default stream config with the user's config.
|
||||
///
|
||||
/// sample_rate: The user's sample rate if it is supported by the device, otherwise the default sample rate.
|
||||
/// channels: The user's number of channels if it is supported by the device, otherwise the default number of channels.
|
||||
/// buffer_size: The user's buffer size if it is supported by the device, otherwise the default buffer size.
|
||||
fn stream_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Result<StreamConfig, anyhow::Error> {
|
||||
if !ALLOWED_SAMPLE_RATES.contains(&sample_rate) {
|
||||
return Err(anyhow!(
|
||||
"Sample rate {} is not supported. Allowed sample rates: {:?}",
|
||||
sample_rate,
|
||||
ALLOWED_SAMPLE_RATES
|
||||
));
|
||||
}
|
||||
if !(channels >= 1 && channels <= MAX_CHANNEL_COUNT) {
|
||||
return Err(anyhow!(
|
||||
"Channel count {} is not supported. Allowed channel counts: 1-{}",
|
||||
channels,
|
||||
MAX_CHANNEL_COUNT
|
||||
));
|
||||
}
|
||||
if !(buffer_size >= MIN_BUFFER_SIZE as u32 && buffer_size <= MAX_BUFFER_SIZE as u32) {
|
||||
return Err(anyhow!(
|
||||
"Buffer size {} is not supported. Allowed buffer sizes: {}-{}",
|
||||
buffer_size,
|
||||
MIN_BUFFER_SIZE,
|
||||
MAX_BUFFER_SIZE
|
||||
));
|
||||
}
|
||||
Ok(StreamConfig {
|
||||
channels,
|
||||
sample_rate: SampleRate(sample_rate),
|
||||
buffer_size: BufferSize::Fixed(buffer_size),
|
||||
})
|
||||
spec: hound::WavSpec,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
current_file: String,
|
||||
max_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// # Recorder
|
||||
///
|
||||
/// The `Recorder` struct is used to record audio.
|
||||
///
|
||||
/// Use `init()` to initialize the recorder, `record()` to start a continuous recording,
|
||||
/// and `rec_secs()` to record for a given number of seconds. The Recorder does not
|
||||
/// need to be reinitialized after a recording is stopped. Calling `record()` or
|
||||
/// `rec_secs()` again will start a new recording with a new filename according to
|
||||
/// the time and date.
|
||||
impl Recorder {
|
||||
|
||||
/// Initializes a new recorder.
|
||||
/// # Init
|
||||
///
|
||||
/// Initializes the recorder with the given host, sample rate, channel count, and buffer size.
|
||||
pub fn init(
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
@ -69,53 +51,52 @@ impl Recorder {
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
buffer_size: u32,
|
||||
interrupt: InterruptHandles,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
max_seconds: Option<u64>,
|
||||
) -> Result<Self, Error> {
|
||||
|
||||
// Select requested host
|
||||
let host = cpal::host_from_id(cpal::available_hosts()
|
||||
.into_iter()
|
||||
.find(|id| *id == host)
|
||||
.ok_or(anyhow!("Requested host device not found"))?
|
||||
)?;
|
||||
// Create interrupt handles to be used by the stream or batch loop.
|
||||
let interrupt_handles = InterruptHandles::new(max_seconds)?;
|
||||
|
||||
// Select requested host.
|
||||
let host = get_host(host)?;
|
||||
|
||||
// Set up the input device and stream with the default input config.
|
||||
let device = host.default_input_device()
|
||||
.ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`",
|
||||
))?;
|
||||
let device = get_device(host)?;
|
||||
|
||||
let default_config = device.default_input_config()?;
|
||||
let user_config = stream_user_config(sample_rate, channels, buffer_size)?;
|
||||
// Get default config for the device.
|
||||
let default_config = get_default_config(&device)?;
|
||||
|
||||
let spec = hound::WavSpec {
|
||||
channels: user_config.channels as _,
|
||||
sample_rate: user_config.sample_rate.0 as _,
|
||||
bits_per_sample: (default_config.sample_format().sample_size() * 8) as _,
|
||||
sample_format: match default_config.sample_format() {
|
||||
cpal::SampleFormat::U16 => hound::SampleFormat::Int,
|
||||
cpal::SampleFormat::I16 => hound::SampleFormat::Int,
|
||||
cpal::SampleFormat::F32 => hound::SampleFormat::Float,
|
||||
},
|
||||
};
|
||||
// Override certain fields of the default stream config with the user's config.
|
||||
let user_config = get_user_config(sample_rate, channels, buffer_size)?;
|
||||
|
||||
// The WAV file we're recording to.
|
||||
let ts: String = Utc::now().format(FMT_TIME).to_string();
|
||||
let filename: String = path.to_str().unwrap().to_owned() + &name + "-" + &ts + ".wav";
|
||||
// Get the hound WAV spec for the user's config.
|
||||
let spec = get_wav_spec(&default_config, &user_config)?;
|
||||
|
||||
Ok(Self {
|
||||
writer: Arc::new(Mutex::new(Some(hound::WavWriter::create(filename.clone(), spec)?))),
|
||||
interrupt,
|
||||
writer: Arc::new(Mutex::new(None)),
|
||||
interrupt_handles,
|
||||
default_config,
|
||||
user_config,
|
||||
device,
|
||||
filename,
|
||||
spec,
|
||||
name,
|
||||
path,
|
||||
current_file: "".to_string(),
|
||||
max_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_stream(&self) -> Result<Stream, anyhow::Error> {
|
||||
fn init_writer(&mut self) -> Result<(), Error> {
|
||||
let filename = get_filename(&self.name, &self.path);
|
||||
self.current_file = filename.clone();
|
||||
self.writer = Arc::new(Mutex::new(Some(hound::WavWriter::create(filename, self.spec)?)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_stream(&self) -> Result<Stream, Error> {
|
||||
let writer = self.writer.clone();
|
||||
let config = self.user_config.clone();
|
||||
let err_fn = |err| { eprintln!("an error occurred on stream: {}", err); };
|
||||
let err_fn = |err| { eprintln!("{}: An error occurred on stream: {}", get_date_time_string(), err); };
|
||||
|
||||
let stream = match self.default_config.sample_format() {
|
||||
cpal::SampleFormat::F32 => self.device.build_input_stream(
|
||||
@ -137,31 +118,45 @@ impl Recorder {
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn record(&self) -> Result<(), anyhow::Error> {
|
||||
/// # Record
|
||||
///
|
||||
/// Start a continuous recording. The recording will be stopped when the
|
||||
/// user presses `Ctrl+C`.
|
||||
pub fn record(&mut self) -> Result<(), Error> {
|
||||
self.init_writer()?;
|
||||
let stream = self.create_stream()?;
|
||||
stream.play()?;
|
||||
println!("REC: {}", self.filename);
|
||||
self.interrupt.stream_wait();
|
||||
println!("REC: {}", self.current_file);
|
||||
self.interrupt_handles.stream_wait();
|
||||
drop(stream);
|
||||
self.writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("STOP: {}", self.filename);
|
||||
println!("STOP: {}", self.current_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_secs(&self, secs: u64) -> Result<(), anyhow::Error> {
|
||||
/// # Record Seconds
|
||||
///
|
||||
/// Record for a given number of seconds or until the user presses `Ctrl+C`.
|
||||
/// Current batch is finished before stopping.
|
||||
pub fn record_secs(&mut self, secs: u64) -> Result<(), Error> {
|
||||
self.init_writer()?;
|
||||
let stream = self.create_stream()?;
|
||||
stream.play()?;
|
||||
println!("REC: {}", self.filename);
|
||||
println!("REC: {}", self.current_file);
|
||||
let now = std::time::Instant::now();
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
while self.interrupt_handles.batch_is_running() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
if now.elapsed().as_secs() >= secs {
|
||||
break;
|
||||
}
|
||||
}
|
||||
drop(stream);
|
||||
self.writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("STOP: {}", self.filename);
|
||||
let writer = self.writer.clone();
|
||||
let current_file = self.current_file.clone();
|
||||
std::thread::spawn(move || {
|
||||
writer.lock().unwrap().take().unwrap().finalize().unwrap();
|
||||
println!("STOP: {}", current_file);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -181,44 +176,68 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn batch_recording(args: &Args, secs: u64, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> {
|
||||
while interrupt_handles.batch_is_running() {
|
||||
let recorder = recorder::Recorder::init(
|
||||
args.name.clone(),
|
||||
match args.output.clone() {
|
||||
Some(path) => path,
|
||||
None => Path::new("./").to_path_buf(),
|
||||
},
|
||||
match args.host {
|
||||
Hosts::Alsa => cpal::HostId::Alsa,
|
||||
Hosts::Jack => cpal::HostId::Jack,
|
||||
},
|
||||
args.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE),
|
||||
args.channels.unwrap_or(DEFAULT_CHANNEL_COUNT),
|
||||
args.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
|
||||
interrupt_handles.clone(),
|
||||
)?;
|
||||
recorder.record_secs(secs)?;
|
||||
fn batch_recording(rec: &mut Recorder, secs: u64) -> Result<(), Error> {
|
||||
let now = std::time::Instant::now();
|
||||
while rec.interrupt_handles.batch_is_running() {
|
||||
match rec.max_seconds {
|
||||
Some(max_secs) => {
|
||||
if now.elapsed().as_secs() >= max_secs {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
rec.record_secs(secs)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contiguous_recording(args: &Args, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> {
|
||||
let recorder = recorder::Recorder::init(
|
||||
fn continuous_recording(rec: &mut Recorder) -> Result<(), Error> {
|
||||
rec.record()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn match_host_platform(host: Hosts) -> Result<cpal::HostId, Error> {
|
||||
match host {
|
||||
Hosts::Alsa => Ok(cpal::HostId::Alsa),
|
||||
Hosts::Jack => Ok(cpal::HostId::Jack),
|
||||
_ => Err(anyhow!("Host not supported on Linux.")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn match_host_platform(host: Hosts) -> Result<cpal::HostId, Error> {
|
||||
match host {
|
||||
Hosts::CoreAudio => Ok(cpal::HostId::CoreAudio),
|
||||
_ => Err(anyhow!("Host not supported on macOS.")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn match_host_platform(host: Hosts) -> Result<cpal::HostId, Error> {
|
||||
match host {
|
||||
Hosts::Asio => Ok(cpal::HostId::Asio),
|
||||
_ => Err(anyhow!("Host not supported on Windows.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record(args: &Rec) -> Result<(), Error> {
|
||||
let mut recorder = Recorder::init(
|
||||
args.name.clone(),
|
||||
match args.output.clone() {
|
||||
Some(path) => path,
|
||||
None => Path::new("./").to_path_buf(),
|
||||
},
|
||||
match args.host {
|
||||
Hosts::Alsa => cpal::HostId::Alsa,
|
||||
Hosts::Jack => cpal::HostId::Jack,
|
||||
},
|
||||
match_host_platform(args.host)?,
|
||||
args.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE),
|
||||
args.channels.unwrap_or(DEFAULT_CHANNEL_COUNT),
|
||||
args.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
|
||||
interrupt_handles.clone(),
|
||||
args.max_seconds,
|
||||
)?;
|
||||
recorder.record()?;
|
||||
Ok(())
|
||||
|
||||
match args.batch_recording {
|
||||
Some(seconds) => batch_recording(&mut recorder, seconds),
|
||||
None => continuous_recording(&mut recorder),
|
||||
}
|
||||
}
|
||||
|
21
configuration/hydrophonitor-config.txt
Normal file
21
configuration/hydrophonitor-config.txt
Normal file
@ -0,0 +1,21 @@
|
||||
# Audio
|
||||
|
||||
SAMPLE_RATE=44100
|
||||
CHANNELS=2
|
||||
BITRATE="192k"
|
||||
BATCH_RECORD_LENGTH=60
|
||||
|
||||
# GPS
|
||||
|
||||
GPS_INTERVAL=5
|
||||
|
||||
# Depth
|
||||
|
||||
DEPTH_INTERVAL=5
|
||||
|
||||
# Output location
|
||||
|
||||
TRY_MOUNT_SSD=true
|
||||
|
||||
DEFAULT_BASE_DIR_PATH=/home/pi/recordings
|
||||
BASE_DIR_PATH=/home/pi/data
|
68
configuration/setup-raspberry-pi.sh
Normal file
68
configuration/setup-raspberry-pi.sh
Normal file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
DIR_PATH=$HOME
|
||||
BOOT_DIR_PATH=/boot/hydrophonitor
|
||||
|
||||
# Copy the files to DIR_PATH
|
||||
echo
|
||||
echo "### Copy files to $DIR_PATH"
|
||||
echo
|
||||
|
||||
mkdir -p "$DIR_PATH"
|
||||
cd "$DIR_PATH"
|
||||
cp -R $BOOT_DIR_PATH/ .
|
||||
|
||||
# Install the Rust toolchain
|
||||
echo
|
||||
echo "### Install the Rust toolchain"
|
||||
echo
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# Install some developer tools
|
||||
echo
|
||||
echo "### Install some developer tools"
|
||||
echo
|
||||
|
||||
sudo apt-get update && sudo apt-get install -y build-essential
|
||||
|
||||
# Setup audio
|
||||
echo
|
||||
echo "### Setup audio"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && sh hydrophonitor/scripts/setup-audio.sh
|
||||
|
||||
# Setup GPS
|
||||
echo
|
||||
echo "### Setup GPS"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && sh hydrophonitor/scripts/setup-gps.sh
|
||||
|
||||
# Setup depth sensor
|
||||
echo
|
||||
echo "### Setup depth recording"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && sh hydrophonitor/scripts/setup-pressure-depth.sh
|
||||
|
||||
# Set up cron job to start the recordings at boot
|
||||
echo
|
||||
echo "### Set up a cron job to start the recordings at boot"
|
||||
echo
|
||||
|
||||
# USER=$(whoami)
|
||||
CRON_FILE=/etc/crontab
|
||||
CRON_COMMAND="@reboot root $DIR_PATH/hydrophonitor/scripts/start-all.sh 2>&1 >> $BOOT_DIR_PATH/log.txt"
|
||||
|
||||
# Append command to cron file only if it's not there yet
|
||||
sudo grep -qxF "$CRON_COMMAND" $CRON_FILE || echo "$CRON_COMMAND" | sudo tee -a $CRON_FILE
|
||||
|
||||
# Reboot
|
||||
echo
|
||||
echo "### Setup ready, run 'sudo reboot' to apply all changes"
|
||||
echo
|
0
configuration/ssh.txt
Normal file
0
configuration/ssh.txt
Normal file
1
configuration/userconf.txt
Normal file
1
configuration/userconf.txt
Normal file
@ -0,0 +1 @@
|
||||
<username>:<encrypted password>
|
13
configuration/wpa_supplicant.conf
Normal file
13
configuration/wpa_supplicant.conf
Normal file
@ -0,0 +1,13 @@
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
country=<Insert 2 letter ISO 3166-1 country code here>
|
||||
|
||||
network={
|
||||
scan_ssid=1
|
||||
ssid="<Name of your wireless LAN>"
|
||||
psk="<Password for your wireless LAN>"
|
||||
proto=RSN
|
||||
key_mgmt=WPA-PSK
|
||||
pairwise=CCMP
|
||||
auth_alg=OPEN
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
time and date, Voltage of depth sensor (V), Depth (m)
|
||||
2022-09-03T17:55:58,0.714,-0.13
|
||||
2022-09-03T17:56:01,0.714,-0.13
|
||||
2022-09-03T17:56:04,0.714,-0.19
|
||||
2022-09-03T17:56:07,0.714,-0.19
|
||||
2022-09-03T17:56:10,0.714,-0.19
|
||||
2022-09-03T17:56:13,0.714,-0.13
|
||||
2022-09-03T17:56:16,0.714,-0.19
|
||||
2022-09-03T17:56:19,0.716,-0.19
|
||||
2022-09-03T17:56:22,0.714,-0.13
|
||||
2022-09-03T17:56:25,0.714,-0.19
|
||||
2022-09-03T17:56:28,0.714,-0.19
|
||||
2022-09-03T17:56:31,0.714,-0.19
|
||||
2022-09-03T17:56:34,0.714,-0.19
|
||||
2022-09-03T17:56:37,0.714,-0.13
|
||||
2022-09-03T17:56:40,0.714,-0.13
|
||||
2022-09-03T17:56:43,0.714,-0.19
|
||||
2022-09-03T17:56:46,0.716,-0.13
|
||||
2022-09-03T17:56:49,0.714,-0.13
|
||||
2022-09-03T17:56:52,0.714,-0.13
|
||||
2022-09-03T17:56:55,0.716,-0.19
|
||||
2022-09-03T17:56:59,0.714,-0.19
|
||||
2022-09-03T17:57:02,0.714,-0.19
|
||||
2022-09-03T17:57:05,0.716,-0.13
|
||||
2022-09-03T17:57:08,0.714,-0.13
|
||||
2022-09-03T17:57:11,0.714,-0.19
|
||||
2022-09-03T17:57:14,0.714,-0.13
|
||||
2022-09-03T17:57:17,0.894,5.48
|
||||
2022-09-03T17:57:20,0.986,8.54
|
||||
2022-09-03T17:57:23,0.988,8.47
|
||||
2022-09-03T17:57:26,1.368,20.58
|
||||
2022-09-03T17:57:29,1.354,20.26
|
||||
2022-09-03T17:57:32,0.714,-0.19
|
||||
2022-09-03T17:57:35,0.716,-0.13
|
||||
2022-09-03T17:57:38,0.714,-0.19
|
||||
2022-09-03T17:57:41,0.714,-0.19
|
||||
2022-09-03T17:57:44,0.714,-0.19
|
||||
2022-09-03T17:57:47,0.716,-0.19
|
||||
2022-09-03T17:57:50,0.714,-0.19
|
||||
2022-09-03T17:57:53,0.714,-0.13
|
||||
2022-09-03T17:57:56,0.714,-0.13
|
||||
2022-09-03T17:57:59,0.714,-0.19
|
||||
2022-09-03T17:58:02,0.714,-0.13
|
|
@ -1,49 +0,0 @@
|
||||
time and date, Voltage of depth sensor (V), Depth (m)
|
||||
2022-09-03T18:01:18,0.716,-0.13
|
||||
2022-09-03T18:01:21,0.716,-0.13
|
||||
2022-09-03T18:01:24,0.714,-0.19
|
||||
2022-09-03T18:01:27,0.716,-0.13
|
||||
2022-09-03T18:01:30,0.714,-0.19
|
||||
2022-09-03T18:01:33,0.716,-0.13
|
||||
2022-09-03T18:01:36,0.716,-0.13
|
||||
2022-09-03T18:01:39,0.716,-0.13
|
||||
2022-09-03T18:01:42,0.716,-0.13
|
||||
2022-09-03T18:01:45,0.714,-0.19
|
||||
2022-09-03T18:01:48,0.714,-0.19
|
||||
2022-09-03T18:01:51,0.714,-0.19
|
||||
2022-09-03T18:01:54,0.714,-0.19
|
||||
2022-09-03T18:01:57,0.716,-0.13
|
||||
2022-09-03T18:02:00,0.716,-0.13
|
||||
2022-09-03T18:02:03,0.714,-0.19
|
||||
2022-09-03T18:02:06,0.714,-0.19
|
||||
2022-09-03T18:02:09,0.714,-0.19
|
||||
2022-09-03T18:02:12,0.714,-0.19
|
||||
2022-09-03T18:02:15,0.714,-0.19
|
||||
2022-09-03T18:02:18,0.716,-0.13
|
||||
2022-09-03T18:02:21,0.716,-0.13
|
||||
2022-09-03T18:02:24,0.714,-0.19
|
||||
2022-09-03T18:02:27,0.714,-0.19
|
||||
2022-09-03T18:02:30,0.714,-0.19
|
||||
2022-09-03T18:02:33,0.714,-0.19
|
||||
2022-09-03T18:02:36,0.714,-0.19
|
||||
2022-09-03T18:02:39,0.716,-0.13
|
||||
2022-09-03T18:02:42,0.714,-0.19
|
||||
2022-09-03T18:02:45,0.714,-0.19
|
||||
2022-09-03T18:02:48,0.714,-0.19
|
||||
2022-09-03T18:02:51,0.714,-0.19
|
||||
2022-09-03T18:02:54,0.714,-0.19
|
||||
2022-09-03T18:02:57,0.714,-0.19
|
||||
2022-09-03T18:03:00,0.714,-0.19
|
||||
2022-09-03T18:03:03,0.714,-0.19
|
||||
2022-09-03T18:03:06,0.714,-0.19
|
||||
2022-09-03T18:03:09,0.714,-0.19
|
||||
2022-09-03T18:03:12,0.714,-0.19
|
||||
2022-09-03T18:03:15,0.714,-0.19
|
||||
2022-09-03T18:03:18,0.716,-0.13
|
||||
2022-09-03T18:03:21,0.716,-0.13
|
||||
2022-09-03T18:03:24,0.714,-0.19
|
||||
2022-09-03T18:03:27,0.716,-0.13
|
||||
2022-09-03T18:03:30,0.714,-0.19
|
||||
2022-09-03T18:03:33,0.714,-0.19
|
||||
2022-09-03T18:03:36,0.714,-0.19
|
||||
2022-09-03T18:03:39,0.714,-0.19
|
|
@ -1,17 +0,0 @@
|
||||
time and date, Voltage of depth sensor (V), Depth (m)
|
||||
2022-09-04T11:50:21,0.716,-0.13
|
||||
2022-09-04T11:50:24,0.716,-0.13
|
||||
2022-09-04T11:50:27,0.716,-0.13
|
||||
2022-09-04T11:50:31,1.12,12.74
|
||||
2022-09-04T11:50:34,0.716,-0.13
|
||||
2022-09-04T11:50:37,1.074,11.28
|
||||
2022-09-04T11:50:40,1.154,13.82
|
||||
2022-09-04T11:50:43,0.716,-0.13
|
||||
2022-09-04T11:50:46,1.034,10.0
|
||||
2022-09-04T11:50:49,1.234,16.37
|
||||
2022-09-04T11:50:52,1.212,15.67
|
||||
2022-09-04T11:50:55,0.714,-0.19
|
||||
2022-09-04T11:50:58,0.716,-0.13
|
||||
2022-09-04T11:51:01,0.716,-0.13
|
||||
2022-09-04T11:51:04,0.716,-0.13
|
||||
2022-09-04T11:51:07,0.716,-0.13
|
|
@ -1,7 +0,0 @@
|
||||
time and date, Voltage of depth sensor (V), Depth (m)
|
||||
2022-09-04T11:56:09,0.716,-0.13
|
||||
2022-09-04T11:56:12,0.716,-0.13
|
||||
2022-09-04T11:56:15,0.716,-0.13
|
||||
2022-09-04T11:56:18,0.716,-0.13
|
||||
2022-09-04T11:56:21,0.716,-0.13
|
||||
2022-09-04T11:56:24,0.716,-0.13
|
|
@ -1,11 +0,0 @@
|
||||
GPStime utc,latitude,longitude,speed,sats in view
|
||||
2022-08-20T09:23:40.000Z,60.196451148,24.960462239,0.582,0
|
||||
2022-08-20T09:23:41.000Z,60.196446687,24.960462239,0.374,0
|
||||
2022-08-20T09:23:42.000Z,60.196437765,24.960462239,0.257,7
|
||||
2022-08-20T09:23:43.000Z,60.196441539,24.960486191,0.192,7
|
||||
2022-08-20T09:23:44.000Z,60.196441539,24.960486191,0.121,7
|
||||
2022-08-20T09:23:45.000Z,60.196437078,24.960486191,0.279,7
|
||||
2022-08-20T09:23:46.000Z,60.196439678,24.960493798,0.233,7
|
||||
2022-08-20T09:23:47.000Z,60.196439678,24.960493798,0.059,7
|
||||
2022-08-20T09:23:48.000Z,60.196439678,24.960493798,0.124,7
|
||||
2022-08-20T09:23:49.000Z,60.196446739,24.960501406,0.369,7
|
|
@ -1,17 +1,23 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import board
|
||||
import time
|
||||
import busio
|
||||
import adafruit_ads1x15.ads1015 as ADS
|
||||
from adafruit_ads1x15.analog_in import AnalogIn
|
||||
from time import sleep, strftime
|
||||
from rpi_lcd import LCD
|
||||
# from rpi_lcd import LCD
|
||||
|
||||
try:
|
||||
lcd = LCD(bus=2)
|
||||
except OSError:
|
||||
lcd = None
|
||||
parser = argparse.ArgumentParser(description='GPS Logger')
|
||||
parser.add_argument('-o', '--output', help='Output directory', required=True)
|
||||
parser.add_argument('-i', '--interval', help='Interval in seconds', required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# try:
|
||||
# lcd = LCD(bus=2)
|
||||
# except OSError:
|
||||
# lcd = None
|
||||
|
||||
# Create the I2C bus
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
@ -19,44 +25,47 @@ i2c = busio.I2C(board.SCL, board.SDA)
|
||||
# Create the ADC object using the I2C bus
|
||||
ads = ADS.ADS1015(i2c)
|
||||
|
||||
# Create a single-ended input on channel 1
|
||||
depthS = AnalogIn(ads, ADS.P1)
|
||||
|
||||
# Create single-ended input on channel 0
|
||||
# tmp36 = AnalogIn(ads, ADS.P0)
|
||||
|
||||
# Attempting to create a single-ended input on channel 1
|
||||
depthS = AnalogIn(ads, ADS.P1)
|
||||
|
||||
# Subtract the offset from the sensor voltage
|
||||
# and convert chan.voltage * (1 degree C / 0.01V) = Degrees Celcius
|
||||
# temperatureC = (tmp36.voltage - 0.5) / 0.01
|
||||
|
||||
# Open the file to write down the results
|
||||
timestr = time.strftime("%Y-%m-%dT%H-%M-%S")
|
||||
filename = "/home/shared/hydrophonitor/data/depth/" + timestr + "_depth_data.csv"
|
||||
# File to write down the results
|
||||
filename = args.output + "/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_depth_data.csv"
|
||||
|
||||
#depthM = ((depthS.voltage * 31.848) - 22.93)
|
||||
interval = int(args.interval) if args.interval else 5
|
||||
|
||||
#Attempting to round the figure to a more intelligible figure
|
||||
#rounddepth = round(depthM, ndigits)
|
||||
#psi = depthS.voltage * 104.1666667 - 75
|
||||
|
||||
#bar = psi * 14.503773800722
|
||||
|
||||
with open(filename, "w", 1) as f:
|
||||
print(f"Writing pressure/depth output to {filename}, interval {interval} seconds", flush=True)
|
||||
f.write("time and date, Voltage of depth sensor (V), Depth (m)\n")
|
||||
|
||||
while True:
|
||||
voltage = depthS.voltage
|
||||
depthM = ((voltage * 31.848) - 22.93)
|
||||
rounddepth = round(depthM, 2)
|
||||
# roundtemp = round(temperatureC, 2)
|
||||
roundvolts = round(voltage, 3)
|
||||
try:
|
||||
while True:
|
||||
voltage = depthS.voltage
|
||||
depthM = ((voltage * 31.848) - 22.93)
|
||||
rounddepth = round(depthM, 2)
|
||||
roundvolts = round(voltage, 3)
|
||||
# roundtemp = round(temperatureC, 2)
|
||||
|
||||
print((str(voltage) + " V ") + (str(depthM) + " m ") + (str(roundvolts) + " V ") + (str(rounddepth) + " m"))
|
||||
print((str(voltage) + " V ") + (str(depthM) + " m ") + (str(roundvolts) + " V ") + (str(rounddepth) + " m"), flush=True)
|
||||
|
||||
if lcd:
|
||||
lcd.clear()
|
||||
lcd.text((str(roundvolts) + " V ") + (str(rounddepth) + " m"), 1)
|
||||
f.write(time.strftime("%Y-%m-%dT%H:%M:%S") + ",")
|
||||
f.write(str(roundvolts) + "," + str(rounddepth) + "\n")
|
||||
|
||||
time.sleep(3)
|
||||
# if lcd:
|
||||
# lcd.clear()
|
||||
# lcd.text((str(roundvolts) + " V ") + (str(rounddepth) + " m"), 1)
|
||||
|
||||
f.write(time.strftime("%Y-%m-%dT%H:%M:%S") + "," + str(roundvolts) + "," + str(rounddepth) + "\n")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit): # when you press ctrl+c
|
||||
print("Exiting depth recording.", flush=True)
|
||||
|
2
depth-logger/requirements.txt
Normal file
2
depth-logger/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Adafruit-Blinka==8.5.0
|
||||
adafruit-circuitpython-ads1x15==2.2.21
|
@ -1,29 +1,39 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from gps import *
|
||||
from time import sleep, strftime
|
||||
import gpsd
|
||||
import time
|
||||
import argparse
|
||||
|
||||
filename = "/home/shared/logger-raspi-setup/data/gps/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_GPS_data.csv"
|
||||
# filename = "/mnt/myssd/GPS_Data" + timestr +".csv"
|
||||
parser = argparse.ArgumentParser(description='GPS Logger')
|
||||
parser.add_argument('-o', '--output', help='Output directory', required=True)
|
||||
parser.add_argument('-i', '--interval', help='Interval in seconds', required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
filename = args.output + "/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_GPS_data.csv"
|
||||
|
||||
interval = int(args.interval) if args.interval else 5
|
||||
|
||||
with open(filename, "w", 1) as f:
|
||||
gpsd = gps(mode=WATCH_ENABLE|WATCH_NEWSTYLE)
|
||||
f.write("GPStime utc,latitude,longitude,speed,sats in view\n")
|
||||
gpsd.connect()
|
||||
|
||||
try:
|
||||
while True:
|
||||
report = gpsd.next()
|
||||
if report["class"] == "TPV":
|
||||
GPStime = str(getattr(report,"time",""))
|
||||
lat = str(getattr(report,"lat",0.0))
|
||||
lon = str(getattr(report,"lon",0.0))
|
||||
speed = str(getattr(report,"speed","nan"))
|
||||
sats = str(len(gpsd.satellites))
|
||||
print(f"Writing GPS output to {filename}, interval {interval} seconds", flush=True)
|
||||
f.write("system_time,gps_time_utc,latitude,longitude,speed,sats_in_view\n")
|
||||
|
||||
f.write(GPStime + "," + lat +"," + lon + "," + speed + "," + sats + "\n")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit): # when you press ctrl+c
|
||||
print("Done.\nExiting.")
|
||||
f.close()
|
||||
while True:
|
||||
try:
|
||||
packet = gpsd.get_current()
|
||||
gps_time_utc = str(packet.time) if packet.mode >= 2 else "-"
|
||||
lat = str(packet.lat) if packet.mode >= 2 else "0.0"
|
||||
lon = str(packet.lon) if packet.mode >= 2 else "0.0"
|
||||
speed = str(packet.hspeed) if packet.mode >= 2 else "0.0"
|
||||
sats = str(packet.sats)
|
||||
system_time = time.strftime("%Y-%m-%dT%H-%M-%S")
|
||||
f.write(f"{system_time},{gps_time_utc},{lat},{lon},{speed},{sats}\n")
|
||||
except (KeyboardInterrupt, SystemExit): # when you press ctrl+c
|
||||
print("Exiting GPS recording.", flush=True)
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"GPS error: {e}", flush=True)
|
||||
|
||||
time.sleep(interval)
|
||||
|
1
gps-logger/requirements.txt
Normal file
1
gps-logger/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
gpsd-py3==0.3.0
|
@ -6,4 +6,9 @@ https://www.circuito.io/app?components=9443,200000,267055
|
||||
|
||||
# Setting up Jack on Raspberry Pi
|
||||
|
||||
https://wiki.linuxaudio.org/wiki/raspberrypi
|
||||
https://wiki.linuxaudio.org/wiki/raspberrypi
|
||||
https://github.com/supercollider/supercollider/blob/develop/README_RASPBERRY_PI.md
|
||||
https://madskjeldgaard.dk/posts/raspi4-notes/
|
||||
|
||||
# Setting up GPS
|
||||
|
||||
|
@ -8,14 +8,10 @@ GOVERNOR="performance"
|
||||
MAX_SPEED="0"
|
||||
MIN_SPEED="0" " | sudo tee -a /etc/default/cpufrequtils
|
||||
|
||||
# Install other useful tools
|
||||
sudo apt-get install htop git perl vim
|
||||
|
||||
# Set CPU governor
|
||||
sudo sed -i 's/exit 0/sudo cpufreq-set -r -g performance/g' /etc/rc.local
|
||||
sudo echo "exit 0" | sudo tee -a /etc/rc.local
|
||||
|
||||
|
||||
# Set realtime priority and memlock
|
||||
sudo echo "
|
||||
@audio nice -15
|
||||
@ -29,4 +25,4 @@ sudo echo "
|
||||
sudo echo "
|
||||
vm.swappiness = 10
|
||||
fs.inotify.max_user_watches = 524288
|
||||
" | sudo tee /etc/sysctl.conf
|
||||
" | sudo tee /etc/sysctl.conf
|
||||
|
5
scripts/export-config-values.sh
Normal file
5
scripts/export-config-values.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONFIG_FILE=$1
|
||||
|
||||
export $(grep -v '^#' $CONFIG_FILE | tr -d '[:space:]' | xargs -d '\n')
|
@ -1,5 +1,28 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# Install jackd
|
||||
echo "Setting up audio recording"
|
||||
|
||||
# Audio setup utils
|
||||
# Install packages
|
||||
sudo apt-get update && sudo apt-get install -y libasound2-dev libjack-dev
|
||||
|
||||
# Get ID and number of the USB audio device
|
||||
card_number=$(aplay -l | grep -i usb | grep -i audio | cut -d ' ' -f 2 | cut -d ':' -f 1)
|
||||
|
||||
# Change default audio device
|
||||
sudo touch /etc/asound.conf
|
||||
|
||||
sudo cat << EOF | sudo tee /etc/asound.conf
|
||||
pcm.!default {
|
||||
type plug
|
||||
slave {
|
||||
pcm "hw:$card_number,0"
|
||||
}
|
||||
}
|
||||
|
||||
ctl.!default {
|
||||
type hw
|
||||
card $card_number
|
||||
}
|
||||
EOF
|
||||
|
||||
cd hydrophonitor/audio-logger && cargo build --release
|
||||
|
@ -1,8 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Setting up GPS recording"
|
||||
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
gpsd gpsd-clients
|
||||
|
||||
sudo pip install -r /home/pi/hydrophonitor/gps-logger/requirements.txt
|
||||
|
||||
sudo systemctl stop gpsd.socket
|
||||
sudo systemctl disable gpsd.socket
|
||||
|
||||
@ -12,5 +16,4 @@ sudo gpsd ${device} -F /var/run/gpsd.sock
|
||||
|
||||
sudo sed -i "s|DEVICES=\"\"|DEVICES=\"${device}\"|g" /etc/default/gpsd
|
||||
|
||||
echo "START_DAEMON=\"true\"" | sudo tee -a /etc/default/gpsd
|
||||
|
||||
sudo grep -qxF "START_DAEMON=\"true\"" /etc/default/gpsd || echo "START_DAEMON=\"true\"" | sudo tee -a /etc/default/gpsd
|
||||
|
11
scripts/setup-pressure-depth.sh
Normal file
11
scripts/setup-pressure-depth.sh
Normal file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Setting up depth recording"
|
||||
|
||||
# Enable i2c bus on Raspberry Pi
|
||||
sudo raspi-config nonint do_i2c 0
|
||||
|
||||
# Install packages
|
||||
sudo apt-get update && sudo apt-get install -y i2c-tools python3-pip
|
||||
|
||||
sudo pip install -r /home/pi/hydrophonitor/depth-logger/requirements.txt
|
@ -1,3 +1,19 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
(trap 'kill 0' SIGINT; ./scripts/start-gps.sh & ./scripts/start-audio.sh)
|
||||
# Print all commands to standard output
|
||||
set -x
|
||||
|
||||
SCRIPT_PATH=/home/pi/hydrophonitor/scripts
|
||||
|
||||
# Export the configuration values
|
||||
$SCRIPT_PATH/export-config-values.sh
|
||||
|
||||
# Create output directory
|
||||
OUTPUT_DIR=$BASE_DIR_PATH/$(date +"%Y-%m-%d_%H-%M-%S_output")
|
||||
echo "Create output directory $OUTPUT_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"/audio
|
||||
|
||||
# Sleep for a little to wait for GPS and sound card to be ready
|
||||
sleep 10
|
||||
|
||||
(export OUTPUT_DIR=$OUTPUT_DIR; $SCRIPT_PATH/start-audio.sh & $SCRIPT_PATH/start-gps.sh & $SCRIPT_PATH/start-pressure-depth.sh) >> "$OUTPUT_DIR"/log.txt 2>&1
|
||||
|
@ -1,9 +1,17 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# Start jack server
|
||||
# Export the configuration values
|
||||
/home/pi/hydrophonitor/scripts/export-config-values.sh
|
||||
|
||||
sh scripts/start-jack.sh
|
||||
AUDIO_TARGET_EXECUTABLE="audio"
|
||||
|
||||
# Start recording
|
||||
OPTIONS="rec \
|
||||
--name audio_data \
|
||||
--output $OUTPUT_DIR/audio \
|
||||
--batch-recording $BATCH_RECORD_LENGTH \
|
||||
--sample-rate $SAMPLE_RATE \
|
||||
--channels $CHANNELS \
|
||||
--buffer-size 1024 \
|
||||
alsa"
|
||||
|
||||
cd audio-logger && cargo run
|
||||
cd /home/pi/hydrophonitor/audio-logger/target/release && ./$AUDIO_TARGET_EXECUTABLE $OPTIONS
|
||||
|
@ -1,3 +1,8 @@
|
||||
#!/usr/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
python /home/shared/logger-raspi-setup/gps-logger/record-gps.py
|
||||
# Export the configuration values
|
||||
/home/pi/hydrophonitor/scripts/export-config-values.sh
|
||||
|
||||
OPTIONS="--output $OUTPUT_DIR --interval $GPS_INTERVAL"
|
||||
|
||||
cd /home/pi/hydrophonitor/gps-logger && python record-gps.py $OPTIONS
|
||||
|
@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Get soundcard name
|
||||
soundcard=$(grep USB /proc/asound/cards | grep -oe "\[.*]" | tr -d "[] ")
|
||||
|
||||
# Start jack server
|
||||
/usr/bin/jackd -P75 -d alsa -d hw:${soundcard} -r 44100 -p 512 -n 3 &
|
8
scripts/start-pressure-depth.sh
Normal file
8
scripts/start-pressure-depth.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Export the configuration values
|
||||
/home/pi/hydrophonitor/scripts/export-config-values.sh
|
||||
|
||||
OPTIONS="--output $OUTPUT_DIR --interval $DEPTH_INTERVAL"
|
||||
|
||||
cd /home/pi/hydrophonitor/depth-logger && python record-depth.py $OPTIONS
|
107
setup.md
107
setup.md
@ -2,52 +2,135 @@
|
||||
|
||||
## Components
|
||||
|
||||
- Raspberry Pi (tested on 4B)
|
||||
- MicroSD card + adapter
|
||||
- Card reader to access the sd card on the computer
|
||||
- Audio card connected via USB and a microphone/hydrophone attached to the audio card
|
||||
- USB GPS receiver
|
||||
- Depth recording components:
|
||||
- Pressure sensor
|
||||
- Adafruit ADS1015 ADC
|
||||
- breadboard, resistors, jumper wires, 12V battery
|
||||
|
||||
|
||||
## Raspberry OS basic setup
|
||||
|
||||
### 1. OS
|
||||
### 1. Install the operating system and set up user, Wi-Fi, ssh access
|
||||
|
||||
#### 1.1 With Raspberry Pi Imager
|
||||
|
||||
The easiest way to install the operating system (Raspberry Pi OS, a Linux Debian-based OS) is to use the official Raspberry Pi Imager utility which works on macOS, Ubuntu and Windows.
|
||||
|
||||
Install from here: https://www.raspberrypi.com/software/
|
||||
|
||||
After installing, plug the SD card to the computer and launch Raspberry Pi Imager.
|
||||
|
||||
Then the following steps:
|
||||
|
||||
1. Select operating system: click Raspberry Pi OS (other) and then, depending on the Pi, either a 32-bit or 64-bit Raspberry Pi OS Lite
|
||||
2. Select storage: the sd card should be listed
|
||||
3. Click the cog icon to set some configurations:
|
||||
- Enable SSH (use password authentication)
|
||||
- Set username and password
|
||||
- Configure wireless LAN: input the name and password of the wi-fi network, select Wireless LAN country
|
||||
- Set locale settings: select options correct for you
|
||||
4. Click Write (all existing data on the SD card will be erased and the OS installed)
|
||||
|
||||
#### 1.2 With some other utility
|
||||
|
||||
If you do not use the Raspberry Pi Imager to set up the SD card, the following steps are required:
|
||||
|
||||
1. Download the 32-bit / 64-bit Rasbperry Pi OS Lite from here: https://www.raspberrypi.com/software/operating-systems/
|
||||
2. Flash the image to the SD card with the utility of your choice (options for Mac, Linux, Windows?)
|
||||
3. Fill in required details in the configuration files in configuration folder and copy them to the boot folder on the SD card (this is the folder that should open when you open the SD card volume on your computer):
|
||||
- ssh.txt: this enables ssh on the Raspberry Pi, no need to edit the file (it's empty, the existence of the file in the boot folder is enough)
|
||||
- userconf.txt: creates a user
|
||||
- replace <username> with the username of choice (e.g. pi)
|
||||
- replace <encrypted password> with an encrypted version of your password which can be created with the openssl command line tool:
|
||||
- open Terminal, write `openssl passwd` and press Enter
|
||||
- input your password and press enter (asked twice)
|
||||
- as output, you will get the encrypted version of the password
|
||||
- wpa_supplicant.conf: set up Wi-Fi
|
||||
- replace <Insert 2 letter ISO 3166-1 country code here> with your country code (e.g. FI)
|
||||
- replace "<Name of your wireless LAN>" with the name of your Wi-Fi network, e.g. "explorersden"
|
||||
- replace "<Password for your wireless LAN>" with the Wi-Fi password, e.g. "password"
|
||||
|
||||
|
||||
### 2. Setting up the recording programs on the Raspberry Pi
|
||||
|
||||
### 2. Users
|
||||
After flashing the operating system to the SD card, it should show up as volume called "boot".
|
||||
|
||||
To install all the needed components and to configure the Raspberry Pi to start the recordings when it is turned on, four steps are needed: copying the needed files to the SD card, putting the SD card in the Raspberry Pi and connecting to it on the command line over SSH, running an installer script on the command line, and finally letting it restart and verify that everything works as intended.
|
||||
|
||||
#### 2.1 Copy files to the SD card, set configuration values
|
||||
|
||||
### 3. Wifi
|
||||
First, set the configuration values in the file hydrophonitor/configuration/hydrophonitor-config.txt. Then, copy the entire `hydrophonitor` folder to the SD card (simple Ctrl+C and Ctrl+V works).
|
||||
|
||||
#### 2.2 Plug the SD card in and connect to the Raspberry Pi over SSH
|
||||
|
||||
Plug the SD card in the Raspberry Pi. Connect the audio card and the GPS receiver over USB to the Raspberry Pi, and plug the power cable. It will take some time for the Raspberry Pi to be ready to accept SSH connections.
|
||||
|
||||
### 4. SSH access
|
||||
To figure out what IP address the Raspberry Pi has been assigned in the local network, a tool called `nmap` is needed.
|
||||
|
||||
To check whether nmap is already installed on the system, open a terminal and run the following command (write it to the terminal and press Enter):
|
||||
|
||||
```
|
||||
nmap --version
|
||||
```
|
||||
|
||||
### 5. Installing needed packages
|
||||
If this prints out version information about nmap (e.g. Nmap version 7.93 ( https://nmap.org)), it is installed. Otherwise, installation instructions can be found here: https://nmap.org/download.html
|
||||
|
||||
After installing, run the following command (it will ask for your user password, write it and press Enter) to find all devices connected to the local network:
|
||||
|
||||
```
|
||||
sudo nmap -sn 192.168.1.0/24
|
||||
```
|
||||
|
||||
## SSD
|
||||
The result will contain a series of discovered devices (hosts) with the following information for each device:
|
||||
|
||||
```
|
||||
Nmap scan report for 192.168.1.108
|
||||
Host is up (0.18s latency).
|
||||
MAC Address: E4:5F:01:B3:65:DE (Raspberry Pi Trading)
|
||||
```
|
||||
|
||||
The Raspberry Pi should show up with its IP address (here, 192.168.1.108), MAC address and a name after the MAC address that should help identifying it (here, it's Raspberry Pi Trading).
|
||||
|
||||
## Real time clock (RTC) module
|
||||
Now, this IP address can be used to connect to the Raspberry Pi over SSH on the command line. Connect by running the command `ssh <user>@<IP address>`, which with a user called `pi` and an IP address of 192.168.1.108 would be
|
||||
|
||||
```
|
||||
ssh pi@192.168.1.108
|
||||
```
|
||||
|
||||
When asked `Are you sure you want to continue connecting (yes/no/[fingerprint])?`, type `yes` and press Enter. Then, write the Raspberry Pi user's password when asked and press Enter.
|
||||
|
||||
## Set up recording
|
||||
After successfully connecting, your prompt should change to `<user>@raspberrypi:~` or something similar.
|
||||
|
||||
### 1. Audio
|
||||
#### 2.3 Run the installer script
|
||||
|
||||
After establishing the SSH connection to the Raspberry Pi, change the current directory to the location of the installer script and run it:
|
||||
|
||||
```
|
||||
cd /boot/hydrophonitor/configuration
|
||||
./setup-raspberry-pi.sh
|
||||
```
|
||||
|
||||
### 2. GPS
|
||||
At the end of successful configuration, the script should print "### Setup ready, run 'sudo reboot' to apply all changes". Run the command and input the Raspberry Pi user's password if requested:
|
||||
|
||||
```
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
This will restart the Raspberry Pi and apply the changes made in the setup. On startup, it should now start recording audio, GPS and depth data.
|
||||
|
||||
### 3. Water pressure
|
||||
### 3. Configuration options
|
||||
|
||||
todo
|
||||
|
||||
### 4. Mount SSD
|
||||
|
||||
todo
|
||||
|
||||
## Test & run
|
||||
|
||||
|
||||
todo
|
||||
|
23
todo.md
Normal file
23
todo.md
Normal file
@ -0,0 +1,23 @@
|
||||
# To do list
|
||||
|
||||
- [ ] Configurable, documented setup process for the Raspberry Pi
|
||||
- [ ] Setup script
|
||||
- [ ] Copy executables & scripts
|
||||
- [ ] Required packages to install
|
||||
- [ ] System configurations
|
||||
- [ ] Configurable values
|
||||
- [ ] Test
|
||||
- [ ] Output formats & location for output data
|
||||
- [ ] Automatic SSD mounting
|
||||
- [ ] Audio recording
|
||||
- [ ] Logging
|
||||
- [ ] Test
|
||||
- [ ] GPS recording
|
||||
- [ ] Logging
|
||||
- [ ] Test
|
||||
- [ ] Depth recording
|
||||
- [ ] Double check formulas to calculate depth & pressure from voltage
|
||||
- [ ] Logging
|
||||
- [ ] Test
|
||||
- [ ] Test autonomous recording as a whole
|
||||
|
Loading…
Reference in New Issue
Block a user