#![warn(missing_docs)]
pub use flexaudio_core as core;
pub mod device_watcher;
mod mix;
pub mod mock;
pub mod stream;
pub use device_watcher::DeviceWatcher;
pub use mock::MockBackend;
pub use stream::Stream;
pub use flexaudio_core::backend::CaptureBackend;
pub use flexaudio_core::types::{
AudioChunk, ChunkFlags, DeviceEvent, DeviceInfo, Error, Event, OutputFormat, ProcessMode,
Result, SourceKind, StreamConfig,
};
pub fn devices() -> Result<Vec<DeviceInfo>> {
#[allow(unused_mut)]
let mut all = flexaudio_mic::list_devices()?;
#[cfg(target_os = "linux")]
{
let linux = flexaudio_os_linux::list_devices()?;
all.extend(linux);
}
#[cfg(target_os = "windows")]
{
let win = flexaudio_os_windows::list_output_devices()?;
all.extend(win);
}
#[cfg(target_os = "macos")]
{
let mac = flexaudio_os_macos::list_output_devices()?;
all.extend(mac);
}
Ok(all)
}
pub fn watch_devices() -> Result<DeviceWatcher> {
device_watcher::watch_devices()
}
pub fn open(config: StreamConfig) -> Result<Stream> {
config.output.validate()?;
let backend = build_backend(&config)?;
Stream::open(config, backend)
}
pub(crate) fn build_backend(config: &StreamConfig) -> Result<Box<dyn CaptureBackend>> {
use flexaudio_core::types::Error;
let backend: Box<dyn CaptureBackend> = match config.kind {
SourceKind::Mic => Box::new(flexaudio_mic::CpalMicBackend::new(config.device_id.clone())),
SourceKind::SystemLoopback => {
build_system_backend(config.exclude_self, config.device_id.clone())?
}
SourceKind::ProcessLoopback => {
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
{
let pid = config.target_pid.ok_or_else(|| {
Error::InvalidArg("ProcessLoopback には target_pid が必要".into())
})?;
#[cfg(target_os = "linux")]
{
Box::new(flexaudio_os_linux::PwProcessBackend::new(pid, config.mode))
}
#[cfg(target_os = "windows")]
{
Box::new(flexaudio_os_windows::WasapiProcessBackend::new(
pid,
config.mode,
))
}
#[cfg(target_os = "macos")]
{
Box::new(flexaudio_os_macos::MacProcessBackend::new(pid, config.mode))
}
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
return Err(Error::Unsupported);
}
}
SourceKind::Mix => {
for (name, gain) in [
("mix_mic_gain", config.mix_mic_gain),
("mix_system_gain", config.mix_system_gain),
] {
if !gain.is_finite() || gain < 0.0 {
return Err(Error::InvalidArg(format!(
"{name} must be finite and >= 0.0, got {gain}"
)));
}
}
let mic = Box::new(flexaudio_mic::CpalMicBackend::new(
config.mix_mic_device_id.clone(),
));
let system =
build_system_backend(config.exclude_self, config.mix_system_device_id.clone())?;
Box::new(mix::CompositeBackend::new(
mic,
system,
config.mix_mic_gain,
config.mix_system_gain,
))
}
};
Ok(backend)
}
fn build_system_backend(
exclude_self: bool,
device_id: Option<String>,
) -> Result<Box<dyn CaptureBackend>> {
#[cfg(target_os = "linux")]
{
Ok(Box::new(flexaudio_os_linux::PwSystemBackend::new(
exclude_self,
device_id,
)))
}
#[cfg(target_os = "windows")]
{
Ok(Box::new(flexaudio_os_windows::WasapiSystemBackend::new(
exclude_self,
device_id,
)))
}
#[cfg(target_os = "macos")]
{
Ok(Box::new(flexaudio_os_macos::MacSystemBackend::new(
exclude_self,
device_id,
)))
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
let _ = (exclude_self, device_id);
Err(flexaudio_core::types::Error::Unsupported)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mix_config_rejects_invalid_side_gains() {
for (mic_gain, system_gain) in [
(-1.0f32, 1.0f32),
(1.0, -0.5),
(f32::NAN, 1.0),
(1.0, f32::INFINITY),
] {
let config = StreamConfig {
kind: SourceKind::Mix,
mix_mic_gain: mic_gain,
mix_system_gain: system_gain,
..Default::default()
};
match open(config) {
Ok(_) => {
panic!("mix ゲイン ({mic_gain}, {system_gain}) は InvalidArg で弾かれるはず")
}
Err(Error::InvalidArg(msg)) => {
assert!(
msg.contains("mix_mic_gain") || msg.contains("mix_system_gain"),
"どちらのゲインが不正か分かるメッセージのはず: {msg}"
);
}
Err(other) => panic!("InvalidArg を期待したが別のエラー: {other:?}"),
}
}
}
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
#[test]
fn mix_config_with_valid_gains_opens() {
let config = StreamConfig {
kind: SourceKind::Mix,
mix_mic_gain: 0.5,
mix_system_gain: 2.0,
..Default::default()
};
let stream = open(config).expect("妥当な Mix config は open まで通るはず");
assert_eq!(stream.native_format(), (48_000, 2));
}
}