From 3524e86bd189aee57da29e3bd5374be0789aeb14 Mon Sep 17 00:00:00 2001 From: Julius Koskela Date: Thu, 22 Sep 2022 07:06:49 +0300 Subject: [PATCH] Implement simple CLI audio logger --- audio-logger/Cargo.lock | 181 +++++++++++++++++------- audio-logger/Cargo.toml | 5 +- audio-logger/Makefile | 17 +++ audio-logger/README.md | 29 ++++ audio-logger/run_test.sh | 10 ++ audio-logger/src/cli.rs | 44 ++++++ audio-logger/src/main.rs | 191 ++++++++++++------------- audio-logger/src/print_configs.rs | 71 ++++++++++ audio-logger/src/recorder.rs | 224 ++++++++++++++++++++++++++++++ 9 files changed, 612 insertions(+), 160 deletions(-) create mode 100644 audio-logger/Makefile create mode 100644 audio-logger/README.md create mode 100644 audio-logger/run_test.sh create mode 100644 audio-logger/src/cli.rs create mode 100644 audio-logger/src/print_configs.rs create mode 100644 audio-logger/src/recorder.rs diff --git a/audio-logger/Cargo.lock b/audio-logger/Cargo.lock index 1e46c57..a7a1b2f 100644 --- a/audio-logger/Cargo.lock +++ b/audio-logger/Cargo.lock @@ -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", diff --git a/audio-logger/Cargo.toml b/audio-logger/Cargo.toml index dc1dfaa..07a5389 100644 --- a/audio-logger/Cargo.toml +++ b/audio-logger/Cargo.toml @@ -6,15 +6,16 @@ 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" nix = "0.23" libc = "0.2.65" parking_lot = "0.12" -jack = { version = "0.9", optional = true } +jack = { version = "0.9", optional = true } \ No newline at end of file diff --git a/audio-logger/Makefile b/audio-logger/Makefile new file mode 100644 index 0000000..8cd27c9 --- /dev/null +++ b/audio-logger/Makefile @@ -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 \ No newline at end of file diff --git a/audio-logger/README.md b/audio-logger/README.md new file mode 100644 index 0000000..5578099 --- /dev/null +++ b/audio-logger/README.md @@ -0,0 +1,29 @@ +# Audio Logger + +CLI-tool for recording audio in a Linux environment. + +## Usage + +```bash + +USAGE: + audio-logger [OPTIONS] --name + +ARGS: + Host API to use [possible values: alsa, jack] + +OPTIONS: + -b, --batch-recording (optional) Will record in [SECONDS] batches + --buffer-size Buffer size in frames + --channels Channels to record + -h, --help Print help information + -n, --name Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav` + -o, --output Path to save the file(s) + --print-configs Output the available devices and their configurations + --sample-rate Sample rate in Hz (default = 44,000Hz) + +``` + +## Testing + +Use the Makefile commands to build the project and run a simple test. diff --git a/audio-logger/run_test.sh b/audio-logger/run_test.sh new file mode 100644 index 0000000..51044ee --- /dev/null +++ b/audio-logger/run_test.sh @@ -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 \ \ No newline at end of file diff --git a/audio-logger/src/cli.rs b/audio-logger/src/cli.rs new file mode 100644 index 0000000..d4c5e9e --- /dev/null +++ b/audio-logger/src/cli.rs @@ -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, + + /// (optional) Will record in [SECONDS] batches + #[clap(short, long, value_name = "SECONDS")] + pub batch_recording: Option, + + /// 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, + + /// Channels to record + #[clap(long, value_name = "CHANNELS")] + pub channels: Option, + + /// Buffer size in frames + #[clap(long, value_name = "FRAMES")] + pub buffer_size: Option, +} diff --git a/audio-logger/src/main.rs b/audio-logger/src/main.rs index 3121f9f..8839314 100644 --- a/audio-logger/src/main.rs +++ b/audio-logger/src/main.rs @@ -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, Condvar)>; +type BatchInterrupt= Arc; + +/// # 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 { + 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::(data, &writer_2), - err_fn, - )?, - cpal::SampleFormat::I16 => device.build_input_stream( - &config_2.into(), - move |data, _: &_| write_input_data::(data, &writer_2), - err_fn, - )?, - cpal::SampleFormat::U16 => device.build_input_stream( - &config_2.into(), - move |data, _: &_| write_input_data::(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(()); } - 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>>>>; - -fn write_input_data(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(); - } - } - } -} diff --git a/audio-logger/src/print_configs.rs b/audio-logger/src/print_configs.rs new file mode 100644 index 0000000..6784749 --- /dev/null +++ b/audio-logger/src/print_configs.rs @@ -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(()) +} \ No newline at end of file diff --git a/audio-logger/src/recorder.rs b/audio-logger/src/recorder.rs new file mode 100644 index 0000000..aa508f9 --- /dev/null +++ b/audio-logger/src/recorder.rs @@ -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>>>>; + +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 { + 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 { + + // 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 { + 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::(data, &writer), + err_fn, + )?, + cpal::SampleFormat::I16 => self.device.build_input_stream( + &config.into(), + move |data, _: &_| write_input_data::(data, &writer), + err_fn, + )?, + cpal::SampleFormat::U16 => self.device.build_input_stream( + &config.into(), + move |data, _: &_| write_input_data::(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(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(()) +}