mod error;
mod processing;
pub use error::{AudioError, AudioResult};
pub use processing::{AudioProcessingOptions, AudioProcessingType};
pub use libwebrtc::audio_source::RtcAudioSource;
use std::fmt;
use std::sync::{Arc, Weak};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RecordingDeviceId(String);
impl RecordingDeviceId {
#[doc(hidden)]
pub fn from_unchecked_guid(guid: &str) -> Self {
Self(guid.to_string())
}
pub fn from_index(index: usize) -> Self {
Self(format!("index:{index}"))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RecordingDeviceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PlayoutDeviceId(String);
impl PlayoutDeviceId {
#[doc(hidden)]
pub fn from_unchecked_guid(guid: &str) -> Self {
Self(guid.to_string())
}
pub fn from_index(index: usize) -> Self {
Self(format!("index:{index}"))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PlayoutDeviceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct RecordingDeviceInfo {
pub id: RecordingDeviceId,
pub name: String,
pub index: usize,
}
#[derive(Debug, Clone)]
pub struct PlayoutDeviceInfo {
pub id: PlayoutDeviceId,
pub name: String,
pub index: usize,
}
use lazy_static::lazy_static;
use parking_lot::Mutex;
use crate::rtc_engine::lk_runtime::LkRuntime;
lazy_static! {
static ref PLATFORM_ADM_HANDLE: Mutex<Weak<PlatformAdmHandle>> = Mutex::new(Weak::new());
}
struct PlatformAdmHandle {
runtime: Arc<LkRuntime>,
}
impl Drop for PlatformAdmHandle {
fn drop(&mut self) {
log::debug!("PlatformAdmHandle dropped - releasing Platform ADM");
self.runtime.release_platform_adm();
log::info!(
"PlatformAdmHandle: released Platform ADM (ref_count now: {})",
self.runtime.platform_adm_ref_count()
);
}
}
#[derive(Clone)]
pub struct PlatformAudio {
handle: Arc<PlatformAdmHandle>,
}
impl PlatformAudio {
pub fn new() -> AudioResult<Self> {
let mut handle_ref = PLATFORM_ADM_HANDLE.lock();
if let Some(handle) = handle_ref.upgrade() {
log::debug!(
"PlatformAudio: reusing existing handle (ref_count: {})",
handle.runtime.platform_adm_ref_count()
);
return Ok(Self { handle });
}
log::debug!("PlatformAudio: creating new handle");
let runtime = LkRuntime::instance();
if !runtime.acquire_platform_adm() {
log::error!("PlatformAudio: failed to acquire Platform ADM");
return Err(AudioError::PlatformInitFailed);
}
log::info!(
"PlatformAudio: acquired Platform ADM (ref_count: {})",
runtime.platform_adm_ref_count()
);
runtime.set_adm_recording_enabled(true);
log::info!("PlatformAudio: enabled ADM recording for microphone capture");
runtime.set_adm_playout_enabled(true);
log::info!("PlatformAudio: enabled ADM playout for platform speakers");
let recording_count = runtime.recording_devices();
let playout_count = runtime.playout_devices();
log::info!(
"PlatformAudio: {} recording devices, {} playout devices",
recording_count,
playout_count
);
let handle = Arc::new(PlatformAdmHandle { runtime });
*handle_ref = Arc::downgrade(&handle);
let audio = Self { handle };
if let Err(e) = audio.configure_audio_processing(AudioProcessingOptions::default()) {
log::warn!("PlatformAudio: failed to configure audio processing: {}", e);
}
Ok(audio)
}
pub fn rtc_source(&self) -> RtcAudioSource {
RtcAudioSource::Device
}
pub fn recording_devices(&self) -> impl Iterator<Item = RecordingDeviceInfo> + '_ {
let count = self.recording_device_count();
(0..count).filter_map(move |index| self.recording_device_info(index))
}
pub fn playout_devices(&self) -> impl Iterator<Item = PlayoutDeviceInfo> + '_ {
let count = self.playout_device_count();
(0..count).filter_map(move |index| self.playout_device_info(index))
}
fn recording_device_count(&self) -> usize {
self.handle.runtime.recording_devices() as usize
}
fn playout_device_count(&self) -> usize {
self.handle.runtime.playout_devices() as usize
}
fn recording_device_info(&self, index: usize) -> Option<RecordingDeviceInfo> {
if index >= self.recording_device_count() {
return None;
}
let index_u16 = index as u16;
let guid = self.handle.runtime.recording_device_guid(index_u16);
let id = if guid.is_empty() {
RecordingDeviceId::from_index(index)
} else {
RecordingDeviceId::from_unchecked_guid(&guid)
};
Some(RecordingDeviceInfo {
id,
name: self.handle.runtime.recording_device_name(index_u16),
index,
})
}
fn playout_device_info(&self, index: usize) -> Option<PlayoutDeviceInfo> {
if index >= self.playout_device_count() {
return None;
}
let index_u16 = index as u16;
let guid = self.handle.runtime.playout_device_guid(index_u16);
let id = if guid.is_empty() {
PlayoutDeviceId::from_index(index)
} else {
PlayoutDeviceId::from_unchecked_guid(&guid)
};
Some(PlayoutDeviceInfo {
id,
name: self.handle.runtime.playout_device_name(index_u16),
index,
})
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
fn ensure_device_exists<Device>(
devices: impl IntoIterator<Item = Device>,
mut is_match: impl FnMut(&Device) -> bool,
) -> AudioResult<()> {
if devices.into_iter().any(|device| is_match(&device)) {
Ok(())
} else {
Err(AudioError::DeviceNotFound)
}
}
pub fn set_recording_device(&self, id: &RecordingDeviceId) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
Self::ensure_device_exists(self.recording_devices(), |device| &device.id == id)?;
let guid = id.as_str();
let success = if let Some(idx_str) = guid.strip_prefix("index:") {
if let Ok(idx) = idx_str.parse::<u16>() {
self.handle.runtime.set_recording_device(idx)
} else {
self.handle.runtime.set_recording_device_by_guid(guid)
}
} else {
self.handle.runtime.set_recording_device_by_guid(guid)
};
if success {
Ok(())
} else {
Err(AudioError::DeviceNotFound)
}
}
pub fn set_recording_device_by_index(&self, index: usize) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
let device = self
.recording_devices()
.find(|d| d.index == index)
.ok_or(AudioError::DeviceNotFound)?;
self.set_recording_device(&device.id)
}
#[cfg(any(target_os = "ios", target_os = "android"))]
{
let id = RecordingDeviceId::from_index(index);
self.set_recording_device(&id)
}
}
pub fn set_playout_device(&self, id: &PlayoutDeviceId) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
Self::ensure_device_exists(self.playout_devices(), |device| &device.id == id)?;
let runtime = &self.handle.runtime;
let guid = id.as_str();
let success = if let Some(idx_str) = guid.strip_prefix("index:") {
if let Ok(idx) = idx_str.parse::<u16>() {
runtime.set_playout_device(idx)
} else {
runtime.set_playout_device_by_guid(guid)
}
} else {
runtime.set_playout_device_by_guid(guid)
};
if !success {
return Err(AudioError::DeviceNotFound);
}
Ok(())
}
pub fn set_playout_device_by_index(&self, index: usize) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
let device = self
.playout_devices()
.find(|d| d.index == index)
.ok_or(AudioError::DeviceNotFound)?;
self.set_playout_device(&device.id)
}
#[cfg(any(target_os = "ios", target_os = "android"))]
{
let id = PlayoutDeviceId::from_index(index);
self.set_playout_device(&id)
}
}
pub fn switch_recording_device(&self, id: &RecordingDeviceId) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
Self::ensure_device_exists(self.recording_devices(), |device| &device.id == id)?;
let runtime = &self.handle.runtime;
let was_initialized = runtime.recording_is_initialized();
if was_initialized {
if !runtime.stop_recording() {
return Err(AudioError::OperationFailed("stop_recording failed".to_string()));
}
}
let guid = id.as_str();
let success = if let Some(idx_str) = guid.strip_prefix("index:") {
if let Ok(idx) = idx_str.parse::<u16>() {
runtime.set_recording_device(idx)
} else {
runtime.set_recording_device_by_guid(guid)
}
} else {
runtime.set_recording_device_by_guid(guid)
};
if !success {
return Err(AudioError::DeviceNotFound);
}
if was_initialized {
if !runtime.init_recording() {
return Err(AudioError::OperationFailed("init_recording failed".to_string()));
}
if !runtime.start_recording() {
return Err(AudioError::OperationFailed("start_recording failed".to_string()));
}
}
Ok(())
}
pub fn switch_recording_device_by_index(&self, index: usize) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
let device = self
.recording_devices()
.find(|d| d.index == index)
.ok_or(AudioError::DeviceNotFound)?;
self.switch_recording_device(&device.id)
}
#[cfg(any(target_os = "ios", target_os = "android"))]
{
let id = RecordingDeviceId::from_index(index);
self.switch_recording_device(&id)
}
}
pub fn switch_playout_device(&self, id: &PlayoutDeviceId) -> AudioResult<()> {
let runtime = &self.handle.runtime;
let was_initialized = runtime.playout_is_initialized();
if was_initialized {
if !runtime.stop_playout() {
return Err(AudioError::OperationFailed("stop_playout failed".to_string()));
}
}
let guid = id.as_str();
let success = if let Some(idx_str) = guid.strip_prefix("index:") {
if let Ok(idx) = idx_str.parse::<u16>() {
runtime.set_playout_device(idx)
} else {
runtime.set_playout_device_by_guid(guid)
}
} else {
runtime.set_playout_device_by_guid(guid)
};
if !success {
return Err(AudioError::DeviceNotFound);
}
if was_initialized {
if !runtime.init_playout() {
return Err(AudioError::OperationFailed("init_playout failed".to_string()));
}
if !runtime.start_playout() {
return Err(AudioError::OperationFailed("start_playout failed".to_string()));
}
}
Ok(())
}
pub fn switch_playout_device_by_index(&self, index: usize) -> AudioResult<()> {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
let device = self
.playout_devices()
.find(|d| d.index == index)
.ok_or(AudioError::DeviceNotFound)?;
self.switch_playout_device(&device.id)
}
#[cfg(any(target_os = "ios", target_os = "android"))]
{
let id = PlayoutDeviceId::from_index(index);
self.switch_playout_device(&id)
}
}
pub fn start_recording(&self) -> AudioResult<()> {
let runtime = &self.handle.runtime;
if !runtime.recording_is_initialized() {
if !runtime.init_recording() {
return Err(AudioError::OperationFailed("init_recording failed".to_string()));
}
}
if runtime.start_recording() {
log::info!("PlatformAudio: started recording");
Ok(())
} else {
Err(AudioError::OperationFailed("start_recording failed".to_string()))
}
}
pub fn stop_recording(&self) -> AudioResult<()> {
let runtime = &self.handle.runtime;
if runtime.stop_recording() {
log::info!("PlatformAudio: stopped recording");
Ok(())
} else {
Err(AudioError::OperationFailed("stop_recording failed".to_string()))
}
}
pub fn is_recording_initialized(&self) -> bool {
self.handle.runtime.recording_is_initialized()
}
pub fn ref_count(&self) -> usize {
Arc::strong_count(&self.handle)
}
pub fn release(self) {
drop(self);
}
pub fn is_hardware_aec_available(&self) -> bool {
self.handle.runtime.builtin_aec_is_available()
}
pub fn is_hardware_agc_available(&self) -> bool {
self.handle.runtime.builtin_agc_is_available()
}
pub fn is_hardware_ns_available(&self) -> bool {
self.handle.runtime.builtin_ns_is_available()
}
pub fn active_aec_type(&self) -> AudioProcessingType {
if self.is_hardware_aec_available() {
AudioProcessingType::Hardware
} else {
AudioProcessingType::Software
}
}
pub fn active_agc_type(&self) -> AudioProcessingType {
if self.is_hardware_agc_available() {
AudioProcessingType::Hardware
} else {
AudioProcessingType::Software
}
}
pub fn active_ns_type(&self) -> AudioProcessingType {
if self.is_hardware_ns_available() {
AudioProcessingType::Hardware
} else {
AudioProcessingType::Software
}
}
pub fn configure_audio_processing(&self, options: AudioProcessingOptions) -> AudioResult<()> {
let runtime = &self.handle.runtime;
let use_hardware = options.prefer_hardware_processing;
if runtime.builtin_aec_is_available() {
let enable_hw = use_hardware && options.echo_cancellation;
if !runtime.enable_builtin_aec(enable_hw) {
log::warn!("enable_builtin_aec({}) failed", enable_hw);
}
}
if runtime.builtin_agc_is_available() {
let enable_hw = use_hardware && options.auto_gain_control;
if !runtime.enable_builtin_agc(enable_hw) {
log::warn!("enable_builtin_agc({}) failed", enable_hw);
}
}
if runtime.builtin_ns_is_available() {
let enable_hw = use_hardware && options.noise_suppression;
if !runtime.enable_builtin_ns(enable_hw) {
log::warn!("enable_builtin_ns({}) failed", enable_hw);
}
}
log::info!(
"Audio processing configured: AEC={}, AGC={}, NS={}, prefer_hw={}",
options.echo_cancellation,
options.auto_gain_control,
options.noise_suppression,
options.prefer_hardware_processing
);
Ok(())
}
pub fn set_echo_cancellation(&self, enable: bool, prefer_hardware: bool) -> AudioResult<()> {
if self.is_hardware_aec_available() {
let enable_hw = enable && prefer_hardware;
if !self.handle.runtime.enable_builtin_aec(enable_hw) {
return Err(AudioError::OperationFailed("enable_builtin_aec failed".to_string()));
}
}
Ok(())
}
pub fn set_auto_gain_control(&self, enable: bool, prefer_hardware: bool) -> AudioResult<()> {
if self.is_hardware_agc_available() {
let enable_hw = enable && prefer_hardware;
if !self.handle.runtime.enable_builtin_agc(enable_hw) {
return Err(AudioError::OperationFailed("enable_builtin_agc failed".to_string()));
}
}
Ok(())
}
pub fn set_noise_suppression(&self, enable: bool, prefer_hardware: bool) -> AudioResult<()> {
if self.is_hardware_ns_available() {
let enable_hw = enable && prefer_hardware;
if !self.handle.runtime.enable_builtin_ns(enable_hw) {
return Err(AudioError::OperationFailed("enable_builtin_ns failed".to_string()));
}
}
Ok(())
}
}
impl fmt::Debug for PlatformAudio {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlatformAudio")
.field("ref_count", &self.ref_count())
.field("recording_device_count", &self.recording_device_count())
.field("playout_device_count", &self.playout_device_count())
.finish()
}
}
pub fn reset_platform_audio() {
let mut handle_ref = PLATFORM_ADM_HANDLE.lock();
*handle_ref = Weak::new();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rtc_audio_source_device_variant() {
let source = RtcAudioSource::Device;
assert!(matches!(source, RtcAudioSource::Device));
}
#[test]
fn recording_device_id_from_index() {
let id = RecordingDeviceId::from_index(0);
assert_eq!(id.as_str(), "index:0");
assert_eq!(id.to_string(), "index:0");
let id = RecordingDeviceId::from_index(42);
assert_eq!(id.as_str(), "index:42");
assert_eq!(id.to_string(), "index:42");
}
#[test]
fn playout_device_id_from_index() {
let id = PlayoutDeviceId::from_index(0);
assert_eq!(id.as_str(), "index:0");
assert_eq!(id.to_string(), "index:0");
let id = PlayoutDeviceId::from_index(7);
assert_eq!(id.as_str(), "index:7");
assert_eq!(id.to_string(), "index:7");
}
#[test]
fn device_id_equality_and_hash() {
use std::collections::HashSet;
let id1 = RecordingDeviceId::from_index(1);
let id2 = RecordingDeviceId::from_unchecked_guid("index:1");
let id3 = RecordingDeviceId::from_index(2);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
let mut set = HashSet::new();
set.insert(id1.clone());
assert!(set.contains(&id2));
assert!(!set.contains(&id3));
}
}