Implement simple CLI audio logger
This commit is contained in:
parent
a6c77f832a
commit
3524e86bd1
181
audio-logger/Cargo.lock
generated
181
audio-logger/Cargo.lock
generated
@ -11,7 +11,7 @@ dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags",
|
||||
"libc",
|
||||
"nix",
|
||||
"nix 0.23.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -26,18 +26,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.62"
|
||||
version = "1.0.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305"
|
||||
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
@ -59,10 +59,11 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"cpal",
|
||||
"ctrlc",
|
||||
"hound",
|
||||
"jack 0.9.2",
|
||||
"libc",
|
||||
"nix",
|
||||
"nix 0.23.1",
|
||||
"parking_lot 0.12.1",
|
||||
]
|
||||
|
||||
@ -167,19 +168,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.17"
|
||||
version = "3.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b"
|
||||
checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
@ -241,7 +257,7 @@ dependencies = [
|
||||
"mach",
|
||||
"ndk",
|
||||
"ndk-glue",
|
||||
"nix",
|
||||
"nix 0.23.1",
|
||||
"oboe",
|
||||
"parking_lot 0.11.2",
|
||||
"stdweb",
|
||||
@ -250,6 +266,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173"
|
||||
dependencies = [
|
||||
"nix 0.25.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
@ -271,7 +297,7 @@ dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"strsim",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -282,7 +308,7 @@ checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -335,6 +361,12 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
@ -346,19 +378,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.4.0"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549"
|
||||
checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.46"
|
||||
version = "0.1.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
|
||||
checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
@ -471,9 +504,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.59"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
|
||||
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@ -492,9 +525,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@ -518,9 +551,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
|
||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
@ -609,7 +642,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -634,6 +667,18 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.1"
|
||||
@ -652,7 +697,7 @@ checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -692,7 +737,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -720,9 +765,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.13.1"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
@ -801,6 +846,30 @@ dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.100",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.4.30"
|
||||
@ -884,9 +953,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.143"
|
||||
version = "1.0.144"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
@ -925,9 +994,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.99"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
|
||||
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
@ -945,28 +1014,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.32"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
|
||||
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.32"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
|
||||
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -991,9 +1060,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
||||
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@ -1001,6 +1070,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
@ -1020,9 +1095,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.82"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
|
||||
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
@ -1030,24 +1105,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.82"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
|
||||
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.82"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
|
||||
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
|
||||
dependencies = [
|
||||
"quote 1.0.21",
|
||||
"wasm-bindgen-macro-support",
|
||||
@ -1055,28 +1130,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.82"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
|
||||
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.99",
|
||||
"syn 1.0.100",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.82"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.59"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1"
|
||||
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
@ -6,11 +6,12 @@ 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"]}
|
||||
cpal= { version = "0.13.5", features = ["jack"] }
|
||||
anyhow = "1.0.61"
|
||||
clap = "3.2.17"
|
||||
hound = "3.4.0"
|
||||
chrono = "0.4.22"
|
||||
ctrlc = "3.2.3"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies]
|
||||
alsa = "0.6"
|
||||
|
17
audio-logger/Makefile
Normal file
17
audio-logger/Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
test: all
|
||||
bash run_test.sh
|
||||
|
||||
all:
|
||||
cargo build --release
|
||||
mkdir -p recordings
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
fclean: clean
|
||||
rm -rf recordings
|
||||
|
||||
re: fclean
|
||||
cargo build --release
|
||||
|
||||
.PHONY: all clean re test
|
29
audio-logger/README.md
Normal file
29
audio-logger/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Audio Logger
|
||||
|
||||
CLI-tool for recording audio in a Linux environment.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
|
||||
USAGE:
|
||||
audio-logger [OPTIONS] --name <NAME> <HOST>
|
||||
|
||||
ARGS:
|
||||
<HOST> Host API to use [possible values: alsa, jack]
|
||||
|
||||
OPTIONS:
|
||||
-b, --batch-recording <SECONDS> (optional) Will record in [SECONDS] batches
|
||||
--buffer-size <FRAMES> Buffer size in frames
|
||||
--channels <CHANNELS> Channels to record
|
||||
-h, --help Print help information
|
||||
-n, --name <NAME> Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav`
|
||||
-o, --output <PATH> Path to save the file(s)
|
||||
--print-configs Output the available devices and their configurations
|
||||
--sample-rate <SAMPLE_RATE> Sample rate in Hz (default = 44,000Hz)
|
||||
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Use the Makefile commands to build the project and run a simple test.
|
10
audio-logger/run_test.sh
Normal file
10
audio-logger/run_test.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
target/release/audio-logger \
|
||||
--name test \
|
||||
--output recordings/ \
|
||||
--batch-recording 3 \
|
||||
--sample-rate 44100 \
|
||||
--channels 2 \
|
||||
--buffer-size 1024 \
|
||||
alsa \
|
44
audio-logger/src/cli.rs
Normal file
44
audio-logger/src/cli.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum Hosts {
|
||||
Alsa,
|
||||
Jack,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about = "A tool to record audio.")]
|
||||
pub struct Args {
|
||||
|
||||
/// Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav`
|
||||
#[clap(required = true, short, long)]
|
||||
pub name: String,
|
||||
|
||||
/// Path to save the file(s)
|
||||
#[clap(long, short, value_parser, value_name = "PATH", value_hint = clap::ValueHint::DirPath)]
|
||||
pub output: Option<std::path::PathBuf>,
|
||||
|
||||
/// (optional) Will record in [SECONDS] batches
|
||||
#[clap(short, long, value_name = "SECONDS")]
|
||||
pub batch_recording: Option<u64>,
|
||||
|
||||
/// Output the available devices and their configurations
|
||||
#[clap(long)]
|
||||
pub print_configs: bool,
|
||||
|
||||
/// Host API to use
|
||||
#[clap(value_enum)]
|
||||
pub host: Hosts,
|
||||
|
||||
/// Sample rate in Hz (default = 44,000Hz)
|
||||
#[clap(long)]
|
||||
pub sample_rate: Option<u32>,
|
||||
|
||||
/// Channels to record
|
||||
#[clap(long, value_name = "CHANNELS")]
|
||||
pub channels: Option<u16>,
|
||||
|
||||
/// Buffer size in frames
|
||||
#[clap(long, value_name = "FRAMES")]
|
||||
pub buffer_size: Option<u32>,
|
||||
}
|
@ -1,116 +1,97 @@
|
||||
//! 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".
|
||||
mod cli;
|
||||
mod recorder;
|
||||
mod print_configs;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use clap::Parser;
|
||||
use recorder::{batch_recording, contiguous_recording};
|
||||
use cli::*;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use chrono::prelude::*;
|
||||
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";
|
||||
|
||||
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> {
|
||||
// Use jack host, requires the jack server to be running
|
||||
let host = cpal::host_from_id(cpal::available_hosts()
|
||||
.into_iter()
|
||||
.find(|id| *id == cpal::HostId::Jack)
|
||||
.expect(
|
||||
"make sure feature jack is specified for cpal. only works on OSes where jack is available",
|
||||
)).expect("jack host unavailable");
|
||||
let args = Args::parse();
|
||||
|
||||
// Set up the input device and stream with the default input config.
|
||||
let device = host.default_input_device()
|
||||
.expect("failed to find input device");
|
||||
|
||||
println!("Input device: {}", device.name()?);
|
||||
|
||||
let config = device
|
||||
.default_input_config()
|
||||
.expect("Failed to get default input config");
|
||||
println!("Default input config: {:?}", config);
|
||||
|
||||
// Location where files will be outputted
|
||||
let path = Path::new("/home/shared/logger-raspi-setup/data/audio/");
|
||||
let spec = wav_spec_from_config(&config);
|
||||
|
||||
for _ in 0..5 {
|
||||
// The WAV file we're recording to.
|
||||
let ts: String = Utc::now().format("%Y-%m-%dT%H-%M-%S.%f").to_string();
|
||||
let file: String = path.to_str().unwrap().to_owned() + &ts + "_audio_data.wav";
|
||||
|
||||
let writer = hound::WavWriter::create(file.clone(), spec)?;
|
||||
let writer = Arc::new(Mutex::new(Some(writer)));
|
||||
|
||||
// Run the input stream on a separate thread.
|
||||
let writer_2 = writer.clone();
|
||||
|
||||
let config_2 = config.clone();
|
||||
|
||||
let err_fn = move |err| {
|
||||
eprintln!("an error occurred on stream: {}", err);
|
||||
};
|
||||
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => device.build_input_stream(
|
||||
&config_2.into(),
|
||||
move |data, _: &_| write_input_data::<f32, f32>(data, &writer_2),
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::I16 => device.build_input_stream(
|
||||
&config_2.into(),
|
||||
move |data, _: &_| write_input_data::<i16, i16>(data, &writer_2),
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::U16 => device.build_input_stream(
|
||||
&config_2.into(),
|
||||
move |data, _: &_| write_input_data::<u16, i16>(data, &writer_2),
|
||||
err_fn,
|
||||
)?,
|
||||
};
|
||||
|
||||
// Start recording
|
||||
println!("Begin recording at {}", Utc::now());
|
||||
stream.play()?;
|
||||
|
||||
// Let recording go for roughly five seconds.
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
drop(stream);
|
||||
writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("Recording {} complete!", file);
|
||||
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)?;
|
||||
},
|
||||
None => {
|
||||
contiguous_recording(&args, interrupt_handles)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat {
|
||||
match format {
|
||||
cpal::SampleFormat::U16 => hound::SampleFormat::Int,
|
||||
cpal::SampleFormat::I16 => hound::SampleFormat::Int,
|
||||
cpal::SampleFormat::F32 => hound::SampleFormat::Float,
|
||||
}
|
||||
}
|
||||
|
||||
fn wav_spec_from_config(config: &cpal::SupportedStreamConfig) -> hound::WavSpec {
|
||||
hound::WavSpec {
|
||||
channels: config.channels() as _,
|
||||
sample_rate: config.sample_rate().0 as _,
|
||||
bits_per_sample: (config.sample_format().sample_size() * 8) as _,
|
||||
sample_format: sample_format(config.sample_format()),
|
||||
}
|
||||
}
|
||||
|
||||
type WavWriterHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
|
||||
|
||||
fn write_input_data<T, U>(input: &[T], writer: &WavWriterHandle)
|
||||
where
|
||||
T: cpal::Sample,
|
||||
U: cpal::Sample + hound::Sample,
|
||||
{
|
||||
if let Ok(mut guard) = writer.try_lock() {
|
||||
if let Some(writer) = guard.as_mut() {
|
||||
for &sample in input.iter() {
|
||||
let sample: U = cpal::Sample::from(&sample);
|
||||
writer.write_sample(sample).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
71
audio-logger/src/print_configs.rs
Normal file
71
audio-logger/src/print_configs.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
|
||||
pub fn print_configs() -> Result<(), anyhow::Error> {
|
||||
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
|
||||
let available_hosts = cpal::available_hosts();
|
||||
println!("Available hosts:\n {:?}", available_hosts);
|
||||
|
||||
for host_id in available_hosts {
|
||||
println!("{}", host_id.name());
|
||||
let host = cpal::host_from_id(host_id)?;
|
||||
|
||||
let default_in = host.default_input_device().map(|e| e.name().unwrap());
|
||||
let default_out = host.default_output_device().map(|e| e.name().unwrap());
|
||||
println!(" Default Input Device:\n {:?}", default_in);
|
||||
println!(" Default Output Device:\n {:?}", default_out);
|
||||
|
||||
let devices = host.devices()?;
|
||||
println!(" Devices: ");
|
||||
for (device_index, device) in devices.enumerate() {
|
||||
println!(" {}. \"{}\"", device_index + 1, device.name()?);
|
||||
|
||||
// Input configs
|
||||
if let Ok(conf) = device.default_input_config() {
|
||||
println!(" Default input stream config:\n {:?}", conf);
|
||||
}
|
||||
let input_configs = match device.supported_input_configs() {
|
||||
Ok(f) => f.collect(),
|
||||
Err(e) => {
|
||||
println!(" Error getting supported input configs: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !input_configs.is_empty() {
|
||||
println!(" All supported input stream configs:");
|
||||
for (config_index, config) in input_configs.into_iter().enumerate() {
|
||||
println!(
|
||||
" {}.{}. {:?}",
|
||||
device_index + 1,
|
||||
config_index + 1,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Output configs
|
||||
if let Ok(conf) = device.default_output_config() {
|
||||
println!(" Default output stream config:\n {:?}", conf);
|
||||
}
|
||||
let output_configs = match device.supported_output_configs() {
|
||||
Ok(f) => f.collect(),
|
||||
Err(e) => {
|
||||
println!(" Error getting supported output configs: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !output_configs.is_empty() {
|
||||
println!(" All supported output stream configs:");
|
||||
for (config_index, config) in output_configs.into_iter().enumerate() {
|
||||
println!(
|
||||
" {}.{}. {:?}",
|
||||
device_index + 1,
|
||||
config_index + 1,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
224
audio-logger/src/recorder.rs
Normal file
224
audio-logger/src/recorder.rs
Normal file
@ -0,0 +1,224 @@
|
||||
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::*;
|
||||
|
||||
type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
|
||||
|
||||
pub struct Recorder {
|
||||
writer: WriteHandle,
|
||||
interrupt: 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),
|
||||
})
|
||||
}
|
||||
|
||||
/// # Recorder
|
||||
///
|
||||
/// The `Recorder` struct is used to record audio.
|
||||
impl Recorder {
|
||||
|
||||
/// Initializes a new recorder.
|
||||
pub fn init(
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
host: HostId,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
buffer_size: u32,
|
||||
interrupt: InterruptHandles,
|
||||
) -> Result<Self, anyhow::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"))?
|
||||
)?;
|
||||
|
||||
// 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 default_config = device.default_input_config()?;
|
||||
let user_config = stream_user_config(sample_rate, channels, buffer_size)?;
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
// 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";
|
||||
|
||||
Ok(Self {
|
||||
writer: Arc::new(Mutex::new(Some(hound::WavWriter::create(filename.clone(), spec)?))),
|
||||
interrupt,
|
||||
default_config,
|
||||
user_config,
|
||||
device,
|
||||
filename,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_stream(&self) -> Result<Stream, anyhow::Error> {
|
||||
let writer = self.writer.clone();
|
||||
let config = self.user_config.clone();
|
||||
let err_fn = |err| { eprintln!("an error occurred on stream: {}", err); };
|
||||
|
||||
let stream = match self.default_config.sample_format() {
|
||||
cpal::SampleFormat::F32 => self.device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<f32, f32>(data, &writer),
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::I16 => self.device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<i16, i16>(data, &writer),
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::U16 => self.device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<u16, i16>(data, &writer),
|
||||
err_fn,
|
||||
)?,
|
||||
};
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn record(&self) -> Result<(), anyhow::Error> {
|
||||
let stream = self.create_stream()?;
|
||||
stream.play()?;
|
||||
println!("REC: {}", self.filename);
|
||||
self.interrupt.stream_wait();
|
||||
drop(stream);
|
||||
self.writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("STOP: {}", self.filename);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_secs(&self, secs: u64) -> Result<(), anyhow::Error> {
|
||||
let stream = self.create_stream()?;
|
||||
stream.play()?;
|
||||
println!("REC: {}", self.filename);
|
||||
let now = std::time::Instant::now();
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
if now.elapsed().as_secs() >= secs {
|
||||
break;
|
||||
}
|
||||
}
|
||||
drop(stream);
|
||||
self.writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("STOP: {}", self.filename);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input_data<T, U>(input: &[T], writer: &WriteHandle)
|
||||
where
|
||||
T: cpal::Sample,
|
||||
U: cpal::Sample + hound::Sample,
|
||||
{
|
||||
if let Ok(mut guard) = writer.try_lock() {
|
||||
if let Some(writer) = guard.as_mut() {
|
||||
for &sample in input.iter() {
|
||||
let sample: U = cpal::Sample::from(&sample);
|
||||
writer.write_sample(sample).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contiguous_recording(args: &Args, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> {
|
||||
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()?;
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user