#![forbid(unsafe_code)]
pub use oxisound_core::{
AudioDevice, CallbackPriority, Channel, ChannelRouting, DefaultSelector, DeviceCapabilities,
DeviceEvent, DeviceInfo, DeviceNotificationCallback, DeviceSelector, DuplexStream, HostApi,
InputStream, LatencyOptimalSelector, MidiDevice, MidiDeviceInfo, MidiInput, MidiMessage,
MidiOutput, NameMatchSelector, NegotiatedConfig, OutputStream, OxiSoundError, SampleFormat,
SessionCategory, SessionInterruptionEvent, StreamConfig, StreamStats,
};
#[cfg(feature = "midi")]
#[must_use = "returns device list; ignoring it means you did the work for nothing"]
pub fn enumerate_midi_devices() -> Result<Vec<MidiDeviceInfo>, OxiSoundError> {
oxisound_midi::MidiDeviceImpl::enumerate_midi()
}
#[cfg(feature = "midi")]
pub fn open_midi_input(port: usize) -> Result<Box<dyn MidiInput>, OxiSoundError> {
<oxisound_midi::MidiDeviceImpl as oxisound_core::MidiDevice>::open_midi_input(port)
}
#[cfg(feature = "midi")]
pub fn open_midi_output(port: usize) -> Result<Box<dyn MidiOutput>, OxiSoundError> {
<oxisound_midi::MidiDeviceImpl as oxisound_core::MidiDevice>::open_midi_output(port)
}
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
pub use oxisound_cpal::DeviceChangeGuard;
#[cfg(feature = "pure")]
pub use oxisound_cpal::{
AdaptiveBufferSizer, CpalCallbackInputStream, CpalCallbackOutputStream, CpalDevice,
CpalDeviceWatcher, CpalOutputStream, StreamHealth,
};
#[cfg(feature = "tokio")]
pub use oxisound_cpal::{CpalAsyncInputStream, CpalAsyncOutputStream};
#[cfg(feature = "tokio")]
pub use oxisound_core::{AsyncInputStream, AsyncOutputStream};
#[must_use = "handle or discard the returned device"]
#[cfg(feature = "pure")]
pub fn default_output() -> Result<CpalDevice, OxiSoundError> {
CpalDevice::default_output()
}
#[must_use = "handle or discard the returned device"]
#[cfg(feature = "pure")]
pub fn default_input() -> Result<CpalDevice, OxiSoundError> {
CpalDevice::default_input()
}
#[must_use = "handle or discard the returned device list"]
#[cfg(feature = "pure")]
pub fn enumerate_devices() -> Result<Vec<DeviceInfo>, OxiSoundError> {
CpalDevice::enumerate()
}
#[must_use = "handle or discard the returned device list"]
#[cfg(feature = "pure")]
pub fn enumerate_input_devices() -> Result<Vec<DeviceInfo>, OxiSoundError> {
CpalDevice::enumerate_input()
}
#[must_use = "handle or discard the returned stream"]
#[cfg(feature = "pure")]
pub fn open_output(config: StreamConfig) -> Result<Box<dyn OutputStream>, OxiSoundError> {
CpalDevice::default_output()?.open_output(config)
}
#[must_use = "handle or discard the returned stream"]
#[cfg(feature = "pure")]
pub fn open_input(config: StreamConfig) -> Result<Box<dyn InputStream>, OxiSoundError> {
CpalDevice::default_input()?.open_input(config)
}
#[must_use = "handle or discard the returned stream"]
#[cfg(feature = "pure")]
pub fn open_loopback(config: StreamConfig) -> Result<Box<dyn InputStream>, OxiSoundError> {
CpalDevice::default_output()?.open_loopback(config)
}
#[must_use = "handle or discard the returned device"]
#[cfg(feature = "pure")]
pub fn select_device(name_fragment: &str) -> Result<CpalDevice, OxiSoundError> {
CpalDevice::select_output(name_fragment)
}
#[must_use = "handle or discard the returned device"]
#[cfg(feature = "pure")]
pub fn select_input_device(name_fragment: &str) -> Result<CpalDevice, OxiSoundError> {
CpalDevice::select_input(name_fragment)
}
#[must_use = "handle or discard the returned stream"]
#[cfg(feature = "pure")]
pub fn duplex_stream(config: StreamConfig) -> Result<Box<dyn DuplexStream>, OxiSoundError> {
CpalDevice::default_output()?.open_duplex(config)
}
#[cfg(feature = "jack")]
pub fn jack_output() -> Result<CpalDevice, OxiSoundError> {
CpalDevice::with_host(HostApi::Jack)
}
#[cfg(feature = "asio")]
pub fn asio_output() -> Result<CpalDevice, OxiSoundError> {
CpalDevice::with_host(HostApi::Asio)
}
#[cfg(feature = "jack-native")]
pub use oxisound_jack::{
JackCallbackOutputStream, JackDevice, JackInputStream, JackMetrics, JackOutputStream,
JackTransportPosition, JackTransportState, MetricsSnapshot, SysExEvent, SysExReassembler,
is_realtime, is_status, midi_message_len,
};
#[cfg(feature = "jack-native")]
pub use oxisound_jack::{JackMidiInput, JackMidiOutput, MIDI_ENTRY_MAX, MidiEntry};
#[cfg(feature = "jack-native")]
#[must_use = "dropping the stream stops playback"]
pub fn jack_native_output(
client_name: &str,
config: StreamConfig,
) -> Result<JackOutputStream, OxiSoundError> {
JackDevice::new(client_name)?.open_output(config)
}
#[cfg(feature = "jack-native")]
#[must_use = "dropping the stream stops capture"]
pub fn jack_native_input(
client_name: &str,
config: StreamConfig,
) -> Result<JackInputStream, OxiSoundError> {
JackDevice::new(client_name)?.open_input(config)
}
#[cfg(feature = "jack-native")]
#[must_use = "dropping the port deactivates the JACK client"]
pub fn jack_midi_output(port_name: &str) -> Result<JackMidiOutput, OxiSoundError> {
JackDevice::new(port_name)?.open_midi_output(port_name)
}
#[cfg(feature = "jack-native")]
#[must_use = "dropping the port deactivates the JACK client"]
pub fn jack_midi_input(port_name: &str) -> Result<JackMidiInput, OxiSoundError> {
JackDevice::new(port_name)?.open_midi_input(port_name)
}
#[cfg(feature = "osc")]
pub use oxisound_osc::{
OscArg, OscBundle, OscError, OscMessage, OscPacket, OscReceiver, OscSender, OscTimeTag,
decode as decode_osc, encode as encode_osc,
};
#[must_use = "handle or discard the returned latency value"]
#[cfg(feature = "pure")]
pub fn latency_ms(device: &CpalDevice) -> Result<f32, OxiSoundError> {
device.default_output_latency_ms()
}
pub fn format_devices(devices: &[DeviceInfo]) -> String {
if devices.is_empty() {
return String::new();
}
let mut out = String::new();
for d in devices {
let marker = if d.is_default { '*' } else { ' ' };
let io = match (d.is_input, d.is_output) {
(true, true) => "in+out",
(true, false) => "in ",
(false, true) => "out ",
(false, false) => " ",
};
let ch_str = if d.channel_counts.is_empty() {
"ch:?".to_string()
} else {
format!(
"ch:[{}]",
d.channel_counts
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(" ")
)
};
let rate_str = if d.sample_rates.is_empty() {
"rates:?".to_string()
} else {
let min = d.sample_rates.iter().copied().min().unwrap_or(0);
let max = d.sample_rates.iter().copied().max().unwrap_or(0);
if min == max {
format!("rates:[{}]", min)
} else {
format!("rates:[{}-{}]", min, max)
}
};
out.push_str(&format!(
"{} [{io}] {ch_str} {rate_str} {name}\n",
marker,
name = d.name
));
}
out
}
#[must_use]
pub fn sine_test_tone(freq_hz: f32, duration_secs: f32, config: StreamConfig) -> Vec<f32> {
let sample_rate = config.sample_rate as f32;
let channels = config.channels as usize;
let total_frames = (sample_rate * duration_secs) as usize;
let total_samples = total_frames * channels;
let mut buf = Vec::with_capacity(total_samples);
for frame in 0..total_frames {
let t = frame as f32;
let sample = (2.0 * std::f32::consts::PI * freq_hz * t / sample_rate).sin();
for _ in 0..channels {
buf.push(sample);
}
}
buf
}
#[must_use]
pub fn white_noise_test(duration_secs: f32, config: StreamConfig) -> Vec<f32> {
let sample_rate = config.sample_rate as usize;
let channels = config.channels as usize;
let total_frames = (sample_rate as f32 * duration_secs) as usize;
let total_samples = total_frames * channels;
let mut buf = Vec::with_capacity(total_samples);
let mut state: u32 = 2_463_534_242; for _ in 0..total_frames {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
let sample = (state as f32 / u32::MAX as f32) * 2.0 - 1.0;
for _ in 0..channels {
buf.push(sample);
}
}
buf
}
#[must_use]
pub fn chirp_test_tone(
f_start: f32,
f_end: f32,
duration_secs: f32,
config: StreamConfig,
) -> Vec<f32> {
let sample_rate = config.sample_rate as f32;
let channels = config.channels as usize;
let total_frames = (sample_rate * duration_secs) as usize;
let total_samples = total_frames * channels;
let mut buf = Vec::with_capacity(total_samples);
for frame in 0..total_frames {
let t = frame as f32 / sample_rate;
let phase = 2.0
* std::f32::consts::PI
* (f_start * t + (f_end - f_start) * t * t / (2.0 * duration_secs));
let sample = phase.sin();
for _ in 0..channels {
buf.push(sample);
}
}
buf
}
#[must_use]
pub fn silence(duration_secs: f32, config: StreamConfig) -> Vec<f32> {
let total_frames = (config.sample_rate as f32 * duration_secs) as usize;
vec![0.0f32; total_frames * config.channels as usize]
}
#[must_use]
pub fn click_track(bpm: f32, duration_secs: f32, config: StreamConfig) -> Vec<f32> {
let sample_rate = config.sample_rate as f32;
let channels = config.channels as usize;
let total_frames = (sample_rate * duration_secs) as usize;
let total_samples = total_frames * channels;
let mut buf = vec![0.0f32; total_samples];
let beat_interval_frames = (sample_rate * 60.0 / bpm) as usize;
let attack_frames = (sample_rate * 0.001) as usize; let decay_frames = (sample_rate * 0.005) as usize;
let mut beat_frame = 0usize;
while beat_frame < total_frames {
for i in 0..attack_frames.min(total_frames.saturating_sub(beat_frame)) {
let amp = i as f32 / attack_frames as f32;
let base = (beat_frame + i) * channels;
if base < total_samples {
for ch in 0..channels {
buf[base + ch] = amp;
}
}
}
for i in 0..decay_frames.min(total_frames.saturating_sub(beat_frame + attack_frames)) {
let amp = 1.0 - (i as f32 / decay_frames as f32);
let base = (beat_frame + attack_frames + i) * channels;
if base < total_samples {
for ch in 0..channels {
buf[base + ch] = amp;
}
}
}
beat_frame += beat_interval_frames;
}
buf
}
#[must_use = "drop the guard to stop the stream"]
#[cfg(feature = "pure")]
pub fn play_callback(
config: StreamConfig,
callback: impl FnMut(&mut [f32]) + Send + 'static,
) -> Result<CpalCallbackOutputStream, OxiSoundError> {
CpalDevice::default_output()?.open_output_callback(config, callback)
}
#[must_use = "drop the guard to stop the stream"]
#[cfg(feature = "pure")]
pub fn capture_callback(
config: StreamConfig,
callback: impl FnMut(&[f32]) + Send + 'static,
) -> Result<CpalCallbackInputStream, OxiSoundError> {
CpalDevice::default_input()?.open_input_callback(config, callback)
}
#[cfg(feature = "pure")]
pub struct DuplexCallbackGuard {
_out: CpalCallbackOutputStream,
_inp: CpalCallbackInputStream,
}
#[must_use = "drop the guard to stop both streams"]
#[cfg(feature = "pure")]
pub fn duplex_callback(
config: StreamConfig,
out_callback: impl FnMut(&mut [f32]) + Send + 'static,
in_callback: impl FnMut(&[f32]) + Send + 'static,
) -> Result<DuplexCallbackGuard, OxiSoundError> {
let _out = CpalDevice::default_output()?.open_output_callback(config.clone(), out_callback)?;
let _inp = CpalDevice::default_input()?.open_input_callback(config, in_callback)?;
Ok(DuplexCallbackGuard { _out, _inp })
}
#[must_use = "handle or discard the returned device"]
#[cfg(feature = "pure")]
pub fn device_by_index(index: usize) -> Result<CpalDevice, OxiSoundError> {
let devices = enumerate_devices()?;
if index < devices.len() {
CpalDevice::select_output(&devices[index].name)
} else {
Err(OxiSoundError::NoDevice)
}
}
#[must_use = "handle or discard the returned config"]
#[cfg(feature = "pure")]
pub fn preferred_output_config() -> Result<StreamConfig, OxiSoundError> {
let device = CpalDevice::default_output()?;
let buf_size = device.optimal_buffer_size().ok();
Ok(StreamConfig {
sample_rate: 48_000,
channels: 2,
buffer_size: buf_size,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
})
}
#[must_use = "handle or discard the returned device list"]
#[cfg(feature = "pure")]
pub fn enumerate_all_devices() -> Result<Vec<DeviceInfo>, OxiSoundError> {
CpalDevice::enumerate_all()
}
#[must_use = "check whether the session was configured successfully"]
pub fn configure_session(category: SessionCategory) -> Result<(), OxiSoundError> {
configure_session_impl(category)
}
#[cfg(feature = "session")]
fn configure_session_impl(category: SessionCategory) -> Result<(), OxiSoundError> {
oxisound_session::configure_session(category)
}
#[cfg(all(target_os = "macos", not(feature = "session")))]
fn configure_session_impl(category: SessionCategory) -> Result<(), OxiSoundError> {
log::debug!(
"configure_session({category:?}): macOS CoreAudio desktop — no AVAudioSession needed. \
Enable the `macos-session` feature for AVFoundation session management."
);
Ok(())
}
#[cfg(all(target_os = "ios", not(feature = "session")))]
fn configure_session_impl(category: SessionCategory) -> Result<(), OxiSoundError> {
let _ = category;
Err(OxiSoundError::UnsupportedConfig(
"configure_session on iOS requires the `macos-session` feature of oxisound".into(),
))
}
#[cfg(not(any(target_os = "macos", target_os = "ios", feature = "session")))]
fn configure_session_impl(category: SessionCategory) -> Result<(), OxiSoundError> {
let _ = category;
Err(OxiSoundError::UnsupportedConfig(
"audio session management requires iOS/macOS".into(),
))
}
pub fn request_microphone_permission() -> Result<bool, OxiSoundError> {
request_microphone_permission_impl()
}
#[cfg(feature = "session")]
fn request_microphone_permission_impl() -> Result<bool, OxiSoundError> {
oxisound_session::request_microphone_permission()
}
#[cfg(all(target_os = "macos", not(feature = "session")))]
fn request_microphone_permission_impl() -> Result<bool, OxiSoundError> {
log::debug!(
"request_microphone_permission: macOS CoreAudio desktop — assumed granted. \
Enable `macos-session` for real TCC permission check."
);
Ok(true)
}
#[cfg(all(target_os = "ios", not(feature = "session")))]
fn request_microphone_permission_impl() -> Result<bool, OxiSoundError> {
Err(OxiSoundError::PermissionDenied(
"request_microphone_permission on iOS requires the `macos-session` feature".into(),
))
}
#[cfg(not(any(target_os = "macos", target_os = "ios", feature = "session")))]
fn request_microphone_permission_impl() -> Result<bool, OxiSoundError> {
Err(OxiSoundError::PermissionDenied(
"microphone permission prompt is not available on this platform".into(),
))
}
#[must_use]
pub fn stream_stats(stream: &dyn oxisound_core::OutputStream) -> Option<StreamStats> {
let s = stream.stats();
if s.frames_processed > 0 || s.underruns > 0 || s.overruns > 0 || s.latency_frames > 0 {
Some(s)
} else {
None
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct MonitorGuard {
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for MonitorGuard {
fn drop(&mut self) {
self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn monitor_stream(
stats_fn: impl Fn() -> StreamStats + Send + 'static,
interval_ms: u64,
callback: impl Fn(StreamStats) + Send + 'static,
) -> MonitorGuard {
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let stop_clone = std::sync::Arc::clone(&stop);
std::thread::spawn(move || {
while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
callback(stats_fn());
std::thread::sleep(std::time::Duration::from_millis(interval_ms));
}
});
MonitorGuard { stop }
}
#[must_use = "handle or discard the returned stream"]
#[cfg(feature = "tokio")]
pub fn async_output(
config: StreamConfig,
) -> Result<impl oxisound_core::AsyncOutputStream, OxiSoundError> {
CpalDevice::default_output()?.open_async_output(config)
}
#[must_use = "handle or discard the returned stream"]
#[cfg(feature = "tokio")]
pub fn capture_stream(
config: StreamConfig,
) -> Result<impl futures_core::Stream<Item = Vec<f32>>, OxiSoundError> {
CpalDevice::default_input()?.open_async_input(config)
}
#[cfg(feature = "tokio")]
pub struct DeviceEventStream {
inner: tokio_stream::wrappers::BroadcastStream<DeviceEvent>,
_watcher: CpalDeviceWatcher,
}
#[cfg(feature = "tokio")]
impl futures_core::Stream for DeviceEventStream {
type Item = DeviceEvent;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
use std::task::Poll;
loop {
match std::pin::Pin::new(&mut self.inner).poll_next(cx) {
Poll::Ready(Some(Ok(event))) => return Poll::Ready(Some(event)),
Poll::Ready(Some(Err(_))) => continue,
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
#[must_use = "drop the returned stream to stop watching"]
#[cfg(feature = "tokio")]
pub fn watch_devices() -> Result<DeviceEventStream, OxiSoundError> {
let (watcher, rx) = CpalDevice::subscribe_device_events()?;
Ok(DeviceEventStream {
inner: tokio_stream::wrappers::BroadcastStream::new(rx),
_watcher: watcher,
})
}
#[must_use = "drop the guard to stop listening for device changes"]
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
pub fn on_device_change(
callback: impl Fn(DeviceEvent) + Send + 'static,
) -> Result<DeviceChangeGuard, OxiSoundError> {
CpalDevice::on_device_change(callback)
}
#[cfg(feature = "smf")]
pub use oxisound_smf::{
Division, SmfError, SmfEvent, SmfFile, SmfFormat, SmfPlayer, SmfTrack, TempoMap, TrackEvent,
parse as parse_smf,
};
#[cfg(feature = "smf")]
pub fn load_smf(data: &[u8]) -> Result<SmfFile, SmfError> {
oxisound_smf::parse(data)
}
#[cfg(all(feature = "smf", feature = "midi"))]
pub fn play_smf(path: &std::path::Path, midi_port: usize) -> Result<(), OxiSoundError> {
let data = std::fs::read(path)?;
let smf = oxisound_smf::parse(&data)
.map_err(|e| OxiSoundError::Stream(format!("SMF parse error: {}", e.0)))?;
let mut output = open_midi_output(midi_port)?;
oxisound_smf::SmfPlayer::new(smf).play(output.as_mut())
}
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
pub struct AutoReconnectGuard {
inner: std::sync::Arc<std::sync::Mutex<Option<CpalOutputStream>>>,
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
stream_config: StreamConfig,
_monitor: std::thread::JoinHandle<()>,
}
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
impl AutoReconnectGuard {
pub fn is_connected(&self) -> bool {
self.inner
.lock()
.is_ok_and(|g| g.as_ref().is_some_and(|s| !s.is_disconnected()))
}
pub fn config(&self) -> StreamConfig {
self.stream_config.clone()
}
}
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
impl oxisound_core::OutputStream for AutoReconnectGuard {
fn write(&mut self, samples: &[f32]) -> Result<(), OxiSoundError> {
let mut guard = self
.inner
.lock()
.map_err(|_| OxiSoundError::Device("mutex poisoned".into()))?;
match guard.as_mut() {
Some(stream) => stream.write(samples),
None => Err(OxiSoundError::Disconnected(
"reconnecting to audio device".into(),
)),
}
}
fn stats(&self) -> oxisound_core::StreamStats {
self.inner
.lock()
.ok()
.and_then(|g| g.as_ref().map(|s| s.stats()))
.unwrap_or_default()
}
}
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
impl Drop for AutoReconnectGuard {
fn drop(&mut self) {
self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
#[must_use = "drop the guard to stop the reconnect monitor"]
#[cfg(all(feature = "pure", not(target_arch = "wasm32")))]
pub fn auto_reconnect_output(config: StreamConfig) -> Result<AutoReconnectGuard, OxiSoundError> {
let initial = CpalDevice::default_output()?.open_output_concrete(config.clone())?;
let inner = std::sync::Arc::new(std::sync::Mutex::new(Some(initial)));
let inner_clone = std::sync::Arc::clone(&inner);
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let stop_clone = std::sync::Arc::clone(&stop);
let monitor_config = config.clone();
let monitor = std::thread::spawn(move || {
const BACKOFF_MS: [u64; 4] = [10, 50, 200, 1000];
let mut backoff_idx = 0usize;
while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
std::thread::sleep(std::time::Duration::from_millis(100));
let is_disconnected = inner_clone
.lock()
.is_ok_and(|g| g.as_ref().is_some_and(|s| s.is_disconnected()));
if is_disconnected {
log::warn!(
"auto_reconnect_output: device disconnected, reconnecting (attempt {})",
backoff_idx + 1
);
let delay = BACKOFF_MS[backoff_idx.min(BACKOFF_MS.len() - 1)];
std::thread::sleep(std::time::Duration::from_millis(delay));
backoff_idx = (backoff_idx + 1).min(BACKOFF_MS.len() - 1);
match CpalDevice::default_output()
.and_then(|dev| dev.open_output_concrete(monitor_config.clone()))
{
Ok(new_stream) => {
if let Ok(mut guard) = inner_clone.lock() {
*guard = Some(new_stream);
backoff_idx = 0;
log::info!("auto_reconnect_output: reconnected successfully");
}
}
Err(e) => {
log::error!("auto_reconnect_output: reconnect attempt failed: {e}");
}
}
} else {
backoff_idx = 0;
}
}
});
Ok(AutoReconnectGuard {
inner,
stop,
stream_config: config,
_monitor: monitor,
})
}