vois/src/audio/capture.rs
loki5512344 aacaf7d9cf Run as 'vois': install via make, config file, Auto-Tune, more presets, custom chains
- 'vois' installs to PATH (make install) and works with no flags:
  auto device selection (pipewire/pulse/default) and 48 kHz default
- ~/.config/vois/config.toml for defaults + custom preset chains (serde)
- Auto-Tune pitch correction (snap to nearest semitone, smoothed)
- 5 new presets: Chipmunk, Helium, Vader, Telephone, Growl (17 total)
- --pitch-correct flag, --no-virtual-mic to disable the default virtual mic
- no dead-code allowances; clippy/fmt/tests clean
2026-08-02 18:20:36 +02:00

82 lines
3.1 KiB
Rust

use anyhow::{Context, Result};
use cpal::traits::{DeviceTrait, HostTrait};
use cpal::{
Device, FromSample, Host, Sample, SampleFormat, SizedSample, Stream, StreamConfig,
SupportedStreamConfig,
};
use ringbuf::traits::Producer;
const PREFERRED_INPUTS: [&str; 3] = ["pipewire", "pulse", "default"];
pub fn find_input(host: &Host, name: Option<&str>) -> Result<Device> {
match name {
Some(name) => host
.input_devices()
.context("failed to enumerate input devices")?
.find(|d| d.name().map(|n| n == name).unwrap_or(false))
.with_context(|| format!("input device {name:?} not found")),
None => {
let mut devices = host
.input_devices()
.context("failed to enumerate input devices")?;
if let Some(device) = devices.find(|d| {
d.name()
.map(|n| PREFERRED_INPUTS.iter().any(|p| n == *p))
.unwrap_or(false)
}) {
return Ok(device);
}
host.default_input_device()
.context("no default input device found")
}
}
}
/// Open the input stream. Captured samples are converted to `f32` (interleaved
/// frames) and pushed into `tx`.
pub fn start<P>(device: &Device, supported: &SupportedStreamConfig, tx: P) -> Result<Stream>
where
P: Producer<Item = f32> + Send + 'static,
{
let config: StreamConfig = supported.config();
match supported.sample_format() {
SampleFormat::F32 => build::<f32, P>(device, &config, tx),
SampleFormat::F64 => build::<f64, P>(device, &config, tx),
SampleFormat::I8 => build::<i8, P>(device, &config, tx),
SampleFormat::I16 => build::<i16, P>(device, &config, tx),
SampleFormat::I32 => build::<i32, P>(device, &config, tx),
SampleFormat::I64 => build::<i64, P>(device, &config, tx),
SampleFormat::U8 => build::<u8, P>(device, &config, tx),
SampleFormat::U16 => build::<u16, P>(device, &config, tx),
SampleFormat::U32 => build::<u32, P>(device, &config, tx),
SampleFormat::U64 => build::<u64, P>(device, &config, tx),
format => anyhow::bail!("unsupported input sample format: {format:?}"),
}
}
fn build<T, P>(device: &Device, config: &StreamConfig, mut tx: P) -> Result<Stream>
where
T: SizedSample,
f32: FromSample<T>,
P: Producer<Item = f32> + Send + 'static,
{
let channels = config.channels as usize;
let mut frame = vec![0f32; channels];
let err_fn = |err| eprintln!("capture stream error: {err}");
device
.build_input_stream(
config,
move |data: &[T], _: &cpal::InputCallbackInfo| {
for chunk in data.chunks(channels) {
for (slot, sample) in frame.iter_mut().zip(chunk) {
*slot = f32::from_sample(*sample);
}
let _ = tx.push_slice(&frame);
}
},
err_fn,
None,
)
.map_err(|e| anyhow::anyhow!("failed to build input stream: {e}"))
}