Organize codebase

This commit is contained in:
Julius Koskela 2022-09-22 17:39:49 +03:00
parent fe14cef7b8
commit 3cc46118ca
5 changed files with 181 additions and 143 deletions

View File

@ -3,8 +3,6 @@ name = "audio-logger"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = {url = "https://github.com/clap-rs/clap", features = ["derive"]}
cpal= { version = "0.13.5", features = ["jack"] }

104
audio-logger/src/getters.rs Normal file
View 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())
}

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

View File

@ -4,6 +4,8 @@
mod cli;
mod recorder;
mod print_configs;
mod getters;
mod input_handling;
use clap::Parser;
use recorder::{batch_recording, contiguous_recording};

View File

@ -1,12 +1,11 @@
use anyhow::anyhow;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::traits::{DeviceTrait, StreamTrait};
use cpal::*;
use std::fs::File;
use std::io::BufWriter;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use chrono::prelude::*;
use super::*;
use crate::getters::*;
use crate::input_handling::*;
type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
@ -22,95 +21,20 @@ pub struct Recorder {
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
///
/// The `Recorder` struct is used to record audio.
///
/// Use `init()` to initialize the recorder, `record()` to start a continuous recording,
/// and `rec_secs()` to record for a given number of seconds. The Recorder does not
/// need to be reinitialized after a recording is stopped. Calling `record()` or
/// `rec_secs()` again will start a new recording with a new filename according to
/// the time and date.
impl Recorder {
/// Initializes a new recorder.
/// # Init
///
/// Initializes the recorder with the given host, sample rate, channel count, and buffer size.
pub fn init(
name: String,
path: PathBuf,
@ -136,7 +60,7 @@ impl Recorder {
let user_config = get_user_config(sample_rate, channels, buffer_size)?;
// 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 {
writer: Arc::new(Mutex::new(None)),
@ -183,6 +107,10 @@ impl Recorder {
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> {
self.init_writer()?;
let stream = self.create_stream()?;
@ -195,6 +123,10 @@ impl Recorder {
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> {
self.init_writer()?;
let stream = self.create_stream()?;
@ -240,55 +172,3 @@ pub fn contiguous_recording(rec: &mut Recorder) -> Result<(), anyhow::Error> {
rec.record()?;
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)
}
}