Organize codebase
This commit is contained in:
parent
fe14cef7b8
commit
3cc46118ca
@ -3,8 +3,6 @@ name = "audio-logger"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = {url = "https://github.com/clap-rs/clap", features = ["derive"]}
|
clap = {url = "https://github.com/clap-rs/clap", features = ["derive"]}
|
||||||
cpal= { version = "0.13.5", features = ["jack"] }
|
cpal= { version = "0.13.5", features = ["jack"] }
|
||||||
|
104
audio-logger/src/getters.rs
Normal file
104
audio-logger/src/getters.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use cpal::traits::{DeviceTrait, HostTrait};
|
||||||
|
use cpal::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use super::*;
|
||||||
|
/// # Get Host
|
||||||
|
///
|
||||||
|
/// Returns the host with the given id if it's available.
|
||||||
|
pub fn get_host(host: HostId) -> Result<Host, anyhow::Error> {
|
||||||
|
Ok(cpal::host_from_id(cpal::available_hosts()
|
||||||
|
.into_iter()
|
||||||
|
.find(|id| *id == host)
|
||||||
|
.ok_or(anyhow!("Requested host device not found"))?
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get Device
|
||||||
|
///
|
||||||
|
/// Returns the default input device for the host if it's available.
|
||||||
|
pub fn get_device(host: Host) -> Result<Device, anyhow::Error> {
|
||||||
|
Ok(host.default_input_device()
|
||||||
|
.ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`",
|
||||||
|
))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get Default Config
|
||||||
|
///
|
||||||
|
/// Get the default config for the given device.
|
||||||
|
pub fn get_default_config(device: &Device) -> Result<SupportedStreamConfig, anyhow::Error> {
|
||||||
|
Ok(device.default_input_config()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get User Config
|
||||||
|
///
|
||||||
|
/// Overrides certain fields of the default stream config with the user's config.
|
||||||
|
///
|
||||||
|
/// sample_rate: The user's sample rate if it is supported by the device, otherwise the default sample rate.
|
||||||
|
/// channels: The user's number of channels if it is supported by the device, otherwise the default number of channels.
|
||||||
|
/// buffer_size: The user's buffer size if it is supported by the device, otherwise the default buffer size.
|
||||||
|
pub fn get_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Result<StreamConfig, 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get WAV Spec
|
||||||
|
///
|
||||||
|
/// Get the WAV spec for the given stream config.
|
||||||
|
pub fn get_wav_spec(default_config: &SupportedStreamConfig, user_config: &StreamConfig) -> Result<hound::WavSpec, anyhow::Error> {
|
||||||
|
Ok(hound::WavSpec {
|
||||||
|
channels: user_config.channels,
|
||||||
|
sample_rate: user_config.sample_rate.0,
|
||||||
|
bits_per_sample: (default_config.sample_format().sample_size() * 8) as u16,
|
||||||
|
sample_format: match default_config.sample_format() {
|
||||||
|
cpal::SampleFormat::U16 => hound::SampleFormat::Int,
|
||||||
|
cpal::SampleFormat::I16 => hound::SampleFormat::Int,
|
||||||
|
cpal::SampleFormat::F32 => hound::SampleFormat::Float,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get Filename
|
||||||
|
///
|
||||||
|
/// Get the filename for the current recording according to the given format,
|
||||||
|
/// the current date and time, and the name prefix.
|
||||||
|
pub fn get_filename(name: &str, path: &PathBuf) -> Result<String, anyhow::Error> {
|
||||||
|
let now: DateTime<Local> = Local::now();
|
||||||
|
let filename = format!(
|
||||||
|
"{}-{}-{}-{}-{}:{}:{}.wav",
|
||||||
|
name,
|
||||||
|
now.year(),
|
||||||
|
now.month(),
|
||||||
|
now.day(),
|
||||||
|
now.hour(),
|
||||||
|
now.minute(),
|
||||||
|
now.second(),
|
||||||
|
);
|
||||||
|
Ok(path.join(filename).to_str().unwrap().to_string())
|
||||||
|
}
|
54
audio-logger/src/input_handling.rs
Normal file
54
audio-logger/src/input_handling.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,8 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod recorder;
|
mod recorder;
|
||||||
mod print_configs;
|
mod print_configs;
|
||||||
|
mod getters;
|
||||||
|
mod input_handling;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use recorder::{batch_recording, contiguous_recording};
|
use recorder::{batch_recording, contiguous_recording};
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use anyhow::anyhow;
|
use cpal::traits::{DeviceTrait, StreamTrait};
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
||||||
use cpal::*;
|
use cpal::*;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use chrono::prelude::*;
|
use crate::getters::*;
|
||||||
use super::*;
|
use crate::input_handling::*;
|
||||||
|
|
||||||
type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
|
type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
|
||||||
|
|
||||||
@ -22,95 +21,20 @@ pub struct Recorder {
|
|||||||
current_file: String,
|
current_file: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_host(host: HostId) -> Result<Host, anyhow::Error> {
|
|
||||||
Ok(cpal::host_from_id(cpal::available_hosts()
|
|
||||||
.into_iter()
|
|
||||||
.find(|id| *id == host)
|
|
||||||
.ok_or(anyhow!("Requested host device not found"))?
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_device(host: Host) -> Result<Device, anyhow::Error> {
|
|
||||||
Ok(host.default_input_device()
|
|
||||||
.ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`",
|
|
||||||
))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_default_config(device: &Device) -> Result<SupportedStreamConfig, anyhow::Error> {
|
|
||||||
Ok(device.default_input_config()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Get User Config
|
|
||||||
///
|
|
||||||
/// Overrides certain fields of the default stream config with the user's config.
|
|
||||||
///
|
|
||||||
/// sample_rate: The user's sample rate if it is supported by the device, otherwise the default sample rate.
|
|
||||||
/// channels: The user's number of channels if it is supported by the device, otherwise the default number of channels.
|
|
||||||
/// buffer_size: The user's buffer size if it is supported by the device, otherwise the default buffer size.
|
|
||||||
fn get_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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_spec(default_config: &SupportedStreamConfig, user_config: &StreamConfig) -> Result<hound::WavSpec, anyhow::Error> {
|
|
||||||
Ok(hound::WavSpec {
|
|
||||||
channels: user_config.channels,
|
|
||||||
sample_rate: user_config.sample_rate.0,
|
|
||||||
bits_per_sample: (default_config.sample_format().sample_size() * 8) as u16,
|
|
||||||
sample_format: match default_config.sample_format() {
|
|
||||||
cpal::SampleFormat::U16 => hound::SampleFormat::Int,
|
|
||||||
cpal::SampleFormat::I16 => hound::SampleFormat::Int,
|
|
||||||
cpal::SampleFormat::F32 => hound::SampleFormat::Float,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_filename(name: &str, path: &PathBuf) -> Result<String, anyhow::Error> {
|
|
||||||
let now: DateTime<Local> = Local::now();
|
|
||||||
let filename = format!(
|
|
||||||
"{}-{}-{}-{}-{}:{}:{}.wav",
|
|
||||||
name,
|
|
||||||
now.year(),
|
|
||||||
now.month(),
|
|
||||||
now.day(),
|
|
||||||
now.hour(),
|
|
||||||
now.minute(),
|
|
||||||
now.second(),
|
|
||||||
);
|
|
||||||
Ok(path.join(filename).to_str().unwrap().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Recorder
|
/// # Recorder
|
||||||
///
|
///
|
||||||
/// The `Recorder` struct is used to record audio.
|
/// The `Recorder` struct is used to record audio.
|
||||||
|
///
|
||||||
|
/// Use `init()` to initialize the recorder, `record()` to start a continuous recording,
|
||||||
|
/// and `rec_secs()` to record for a given number of seconds. The Recorder does not
|
||||||
|
/// need to be reinitialized after a recording is stopped. Calling `record()` or
|
||||||
|
/// `rec_secs()` again will start a new recording with a new filename according to
|
||||||
|
/// the time and date.
|
||||||
impl Recorder {
|
impl Recorder {
|
||||||
|
|
||||||
/// Initializes a new recorder.
|
/// # Init
|
||||||
|
///
|
||||||
|
/// Initializes the recorder with the given host, sample rate, channel count, and buffer size.
|
||||||
pub fn init(
|
pub fn init(
|
||||||
name: String,
|
name: String,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@ -136,7 +60,7 @@ impl Recorder {
|
|||||||
let user_config = get_user_config(sample_rate, channels, buffer_size)?;
|
let user_config = get_user_config(sample_rate, channels, buffer_size)?;
|
||||||
|
|
||||||
// Get the hound WAV spec for the user's config.
|
// Get the hound WAV spec for the user's config.
|
||||||
let spec = get_spec(&default_config, &user_config)?;
|
let spec = get_wav_spec(&default_config, &user_config)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
writer: Arc::new(Mutex::new(None)),
|
writer: Arc::new(Mutex::new(None)),
|
||||||
@ -183,6 +107,10 @@ impl Recorder {
|
|||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Record
|
||||||
|
///
|
||||||
|
/// Start a continuous recording. The recording will be stopped when the
|
||||||
|
/// user presses `Ctrl+C`.
|
||||||
pub fn record(&mut self) -> Result<(), anyhow::Error> {
|
pub fn record(&mut self) -> Result<(), anyhow::Error> {
|
||||||
self.init_writer()?;
|
self.init_writer()?;
|
||||||
let stream = self.create_stream()?;
|
let stream = self.create_stream()?;
|
||||||
@ -195,6 +123,10 @@ impl Recorder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Record Seconds
|
||||||
|
///
|
||||||
|
/// Record for a given number of seconds or until the user presses `Ctrl+C`.
|
||||||
|
/// Current batch is finished before stopping.
|
||||||
pub fn record_secs(&mut self, secs: u64) -> Result<(), anyhow::Error> {
|
pub fn record_secs(&mut self, secs: u64) -> Result<(), anyhow::Error> {
|
||||||
self.init_writer()?;
|
self.init_writer()?;
|
||||||
let stream = self.create_stream()?;
|
let stream = self.create_stream()?;
|
||||||
@ -240,55 +172,3 @@ pub fn contiguous_recording(rec: &mut Recorder) -> Result<(), anyhow::Error> {
|
|||||||
rec.record()?;
|
rec.record()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user