Implement simple CLI audio logger

This commit is contained in:
Julius Koskela 2022-09-22 07:06:49 +03:00
parent a6c77f832a
commit 3524e86bd1
9 changed files with 612 additions and 160 deletions

181
audio-logger/Cargo.lock generated
View File

@ -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",

View File

@ -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
View 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
View 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
View 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
View 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>,
}

View File

@ -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();
}
}
}
}

View 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(())
}

View 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(())
}