Improve code clarity and save configuration for when writer is reset

This commit is contained in:
Julius Koskela 2022-09-22 17:25:10 +03:00
parent 3524e86bd1
commit fe14cef7b8
2 changed files with 162 additions and 132 deletions

View File

@ -10,7 +10,7 @@ use recorder::{batch_recording, contiguous_recording};
use cli::*; use cli::*;
use std::path::Path; use std::path::Path;
use print_configs::*; use print_configs::*;
use std::sync::{Arc, Mutex, Condvar, atomic::{AtomicBool, Ordering}}; use std::sync::{Condvar, atomic::{AtomicBool, Ordering}};
const DEFAULT_SAMPLE_RATE: u32 = 44100; const DEFAULT_SAMPLE_RATE: u32 = 44100;
const DEFAULT_CHANNEL_COUNT: u16 = 1; const DEFAULT_CHANNEL_COUNT: u16 = 1;
@ -19,59 +19,6 @@ const ALLOWED_SAMPLE_RATES: &[u32] = &[44100, 48000, 88200, 96000, 176400, 19200
const MAX_CHANNEL_COUNT: u16 = 2; const MAX_CHANNEL_COUNT: u16 = 2;
const MIN_BUFFER_SIZE: usize = 64; const MIN_BUFFER_SIZE: usize = 64;
const MAX_BUFFER_SIZE: usize = 8192; 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> { fn main() -> Result<(), anyhow::Error> {
let args = Args::parse(); let args = Args::parse();
@ -81,14 +28,27 @@ fn main() -> Result<(), anyhow::Error> {
return Ok(()); return Ok(());
} }
let interrupt_handles = InterruptHandles::new()?; let mut 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),
)?;
match args.batch_recording { match args.batch_recording {
Some(secs) => { Some(secs) => {
batch_recording(&args, secs, interrupt_handles)?; batch_recording(&mut recorder, secs)?;
}, },
None => { None => {
contiguous_recording(&args, interrupt_handles)?; contiguous_recording(&mut recorder)?;
} }
} }

View File

@ -12,21 +12,42 @@ type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
pub struct Recorder { pub struct Recorder {
writer: WriteHandle, writer: WriteHandle,
interrupt: InterruptHandles, interrupt_handles: InterruptHandles,
default_config: SupportedStreamConfig, default_config: SupportedStreamConfig,
user_config: StreamConfig, user_config: StreamConfig,
device: Device, device: Device,
filename: String, spec: hound::WavSpec,
name: String,
path: PathBuf,
current_file: String,
} }
/// # Stream User Config 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. /// 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. /// 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. /// 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. /// 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> { fn get_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Result<StreamConfig, anyhow::Error> {
if !ALLOWED_SAMPLE_RATES.contains(&sample_rate) { if !ALLOWED_SAMPLE_RATES.contains(&sample_rate) {
return Err(anyhow!( return Err(anyhow!(
"Sample rate {} is not supported. Allowed sample rates: {:?}", "Sample rate {} is not supported. Allowed sample rates: {:?}",
@ -56,6 +77,34 @@ fn stream_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Resu
}) })
} }
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.
@ -69,53 +118,50 @@ impl Recorder {
sample_rate: u32, sample_rate: u32,
channels: u16, channels: u16,
buffer_size: u32, buffer_size: u32,
interrupt: InterruptHandles,
) -> Result<Self, anyhow::Error> { ) -> Result<Self, anyhow::Error> {
// Select requested host // Create interrupt handles to be used by the stream or batch loop.
let host = cpal::host_from_id(cpal::available_hosts() let interrupt_handles = InterruptHandles::new()?;
.into_iter()
.find(|id| *id == host) // Select requested host.
.ok_or(anyhow!("Requested host device not found"))? let host = get_host(host)?;
)?;
// Set up the input device and stream with the default input config. // Set up the input device and stream with the default input config.
let device = host.default_input_device() let device = get_device(host)?;
.ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`",
))?;
let default_config = device.default_input_config()?; // Get default config for the device.
let user_config = stream_user_config(sample_rate, channels, buffer_size)?; let default_config = get_default_config(&device)?;
let spec = hound::WavSpec { // Override certain fields of the default stream config with the user's config.
channels: user_config.channels as _, let user_config = get_user_config(sample_rate, channels, buffer_size)?;
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. // Get the hound WAV spec for the user's config.
let ts: String = Utc::now().format(FMT_TIME).to_string(); let spec = get_spec(&default_config, &user_config)?;
let filename: String = path.to_str().unwrap().to_owned() + &name + "-" + &ts + ".wav";
Ok(Self { Ok(Self {
writer: Arc::new(Mutex::new(Some(hound::WavWriter::create(filename.clone(), spec)?))), writer: Arc::new(Mutex::new(None)),
interrupt, interrupt_handles,
default_config, default_config,
user_config, user_config,
device, device,
filename, spec,
name,
path,
current_file: "".to_string(),
}) })
} }
fn init_writer(&mut self) -> Result<(), anyhow::Error> {
let filename = get_filename(&self.name, &self.path)?;
self.current_file = filename.clone();
*self.writer.lock().unwrap() = Some(hound::WavWriter::create(filename, self.spec)?);
Ok(())
}
fn create_stream(&self) -> Result<Stream, anyhow::Error> { fn create_stream(&self) -> Result<Stream, anyhow::Error> {
let writer = self.writer.clone(); let writer = self.writer.clone();
let config = self.user_config.clone(); let config = self.user_config.clone();
let err_fn = |err| { eprintln!("an error occurred on stream: {}", err); }; let err_fn = |err| { eprintln!("An error occurred on stream: {}", err); };
let stream = match self.default_config.sample_format() { let stream = match self.default_config.sample_format() {
cpal::SampleFormat::F32 => self.device.build_input_stream( cpal::SampleFormat::F32 => self.device.build_input_stream(
@ -137,21 +183,23 @@ impl Recorder {
Ok(stream) Ok(stream)
} }
pub fn record(&self) -> Result<(), anyhow::Error> { pub fn record(&mut self) -> Result<(), anyhow::Error> {
self.init_writer()?;
let stream = self.create_stream()?; let stream = self.create_stream()?;
stream.play()?; stream.play()?;
println!("REC: {}", self.filename); println!("REC: {}", self.current_file);
self.interrupt.stream_wait(); self.interrupt_handles.stream_wait();
drop(stream); drop(stream);
self.writer.lock().unwrap().take().unwrap().finalize()?; self.writer.lock().unwrap().take().unwrap().finalize()?;
println!("STOP: {}", self.filename); println!("STOP: {}", self.current_file);
Ok(()) Ok(())
} }
pub fn record_secs(&self, secs: u64) -> Result<(), anyhow::Error> { pub fn record_secs(&mut self, secs: u64) -> Result<(), anyhow::Error> {
self.init_writer()?;
let stream = self.create_stream()?; let stream = self.create_stream()?;
stream.play()?; stream.play()?;
println!("REC: {}", self.filename); println!("REC: {}", self.current_file);
let now = std::time::Instant::now(); let now = std::time::Instant::now();
loop { loop {
std::thread::sleep(std::time::Duration::from_millis(500)); std::thread::sleep(std::time::Duration::from_millis(500));
@ -161,7 +209,7 @@ impl Recorder {
} }
drop(stream); drop(stream);
self.writer.lock().unwrap().take().unwrap().finalize()?; self.writer.lock().unwrap().take().unwrap().finalize()?;
println!("STOP: {}", self.filename); println!("STOP: {}", self.current_file);
Ok(()) Ok(())
} }
} }
@ -181,44 +229,66 @@ where
} }
} }
pub fn batch_recording(args: &Args, secs: u64, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> { pub fn batch_recording(rec: &mut Recorder, secs: u64) -> Result<(), anyhow::Error> {
while interrupt_handles.batch_is_running() { while rec.interrupt_handles.batch_is_running() {
let recorder = recorder::Recorder::init( rec.record_secs(secs)?;
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(()) Ok(())
} }
pub fn contiguous_recording(args: &Args, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> { pub fn contiguous_recording(rec: &mut Recorder) -> Result<(), anyhow::Error> {
let recorder = recorder::Recorder::init( rec.record()?;
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(()) 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)
}
}