refactor(audio): share AudioHandler between iOS and macOS, bump deps
crates/chanora_audio/src/engine.rs: drop the macOS-specific event-queue producer/consumer path; macOS now uses the iOS-style direct AudioHandler::fill_buffer in the VPIO render callback. The shared AudioHandler is an Arc<Mutex<...>>; the realtime callback uses try_lock so it never blocks on the tokio decode task (see ios_voice_unit.rs render callback). crates/chanora_audio/src/mobile_voice_backend.rs: update VoiceAudioParams cfg gates — handler is now the iOS/macOS/desktop shape (Arc<Mutex<AudioHandler<SessionAudioId>>>), event_producer is Android-only. crates/chanora_audio/src/lib.rs: widen the audio_event_queue module visibility to test so the macOS-specific path can be exercised by the unit test suite. Cargo.toml: bump cpal 0.17.3 -> 0.18.0, jni 0.21 -> 0.22.4, windows 0.54 -> 0.62, criterion 0.5 -> 0.8. Cargo.lock follows.
This commit is contained in:
@@ -52,7 +52,7 @@ use tsclientlib::audio::AudioHandler;
|
||||
|
||||
use chanora_protocol::{InboundVoice, OutPacket};
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "macos"))]
|
||||
#[cfg(target_os = "android")]
|
||||
use crate::audio_event_queue::{AudioCommand, AudioEventQueue, AudioPacket};
|
||||
use crate::AudioError;
|
||||
|
||||
@@ -307,9 +307,9 @@ pub struct AudioEngine {
|
||||
output_muted: Arc<AtomicBool>,
|
||||
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
#[cfg(not(any(target_os = "android", target_os = "macos")))]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
||||
#[cfg(any(target_os = "android", target_os = "macos"))]
|
||||
#[cfg(target_os = "android")]
|
||||
audio_event_producer: crate::audio_event_queue::AudioEventProducer,
|
||||
#[cfg(target_os = "android")]
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
@@ -1246,25 +1246,16 @@ impl AudioEngine {
|
||||
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
|
||||
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
let audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>> =
|
||||
Arc::new(Mutex::new(AudioHandler::new()));
|
||||
#[cfg(target_os = "macos")]
|
||||
let event_queue = AudioEventQueue::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
let event_producer = AudioEventQueue::producer(&event_queue);
|
||||
let voice_out_tx_for_backend = voice_out_tx.clone();
|
||||
|
||||
// Construct the live iOS voice backend. Platform VPIO stays
|
||||
// the default shipping path; Sonora/RemoteIO remains opt-in.
|
||||
let ios_voice_backend =
|
||||
open_ios_voice_backend(crate::mobile_voice_backend::VoiceAudioParams {
|
||||
#[cfg(target_os = "ios")]
|
||||
handler: audio_handler.clone(),
|
||||
#[cfg(target_os = "macos")]
|
||||
handler: AudioHandler::new(),
|
||||
#[cfg(target_os = "macos")]
|
||||
event_producer: event_producer.clone(),
|
||||
output_gain: output_gain.clone(),
|
||||
output_muted: output_muted.clone(),
|
||||
voice_out_tx: voice_out_tx_for_backend,
|
||||
@@ -1289,10 +1280,7 @@ impl AudioEngine {
|
||||
// waiting.
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let frames_received_for_task = frames_received.clone();
|
||||
#[cfg(target_os = "ios")]
|
||||
let handler_for_task = audio_handler.clone();
|
||||
#[cfg(target_os = "macos")]
|
||||
let event_producer_for_task = event_producer.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -1304,21 +1292,16 @@ impl AudioEngine {
|
||||
match item {
|
||||
Some(v) => {
|
||||
let id = SessionAudioId(v.from_client);
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
let mut h = handler_for_task.lock().unwrap();
|
||||
if let Err(e) = h.handle_packet(id, v.packet) {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
} else {
|
||||
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let packet = AudioPacket { client_id: id, data: v.packet };
|
||||
if event_producer_for_task.push_packet(packet).is_ok() {
|
||||
let res = h.handle_packet(id, v.packet);
|
||||
drop(h);
|
||||
match res {
|
||||
Ok(_) => {
|
||||
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
@@ -1336,10 +1319,7 @@ impl AudioEngine {
|
||||
output_muted,
|
||||
audio_processing_config,
|
||||
audio_processing_stats,
|
||||
#[cfg(target_os = "ios")]
|
||||
audio_handler,
|
||||
#[cfg(target_os = "macos")]
|
||||
audio_event_producer: event_producer,
|
||||
_ios_voice_backend: Mutex::new(Some(ios_voice_backend)),
|
||||
shutdown_tx: Some(shutdown_tx),
|
||||
capture_active,
|
||||
@@ -1693,7 +1673,7 @@ impl AudioEngine {
|
||||
/// `0.0..4.0`.
|
||||
pub fn set_client_volume(&self, client_id: u64, volume: f32) {
|
||||
let clamped = volume.clamp(0.0, 4.0);
|
||||
#[cfg(any(target_os = "android", target_os = "macos"))]
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped);
|
||||
for _ in 0..64 {
|
||||
@@ -1712,7 +1692,7 @@ impl AudioEngine {
|
||||
"set_client_volume: control queue full after 64 retries — volume not applied"
|
||||
);
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "macos")))]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
match self.audio_handler.lock() {
|
||||
Ok(mut h) => {
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "android", test))]
|
||||
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
|
||||
mod audio_event_queue;
|
||||
pub mod audio_processing;
|
||||
pub mod debug_wav;
|
||||
|
||||
@@ -78,15 +78,18 @@ pub(crate) struct VoiceAudioParams {
|
||||
pub frames_sent: Arc<AtomicU32>,
|
||||
/// Pre-encode amplitude scale (1.0 = unity).
|
||||
pub mic_gain: f32,
|
||||
/// AudioHandler owned by the Android/macOS output callback.
|
||||
#[cfg(any(target_os = "android", target_os = "macos"))]
|
||||
/// AudioHandler owned by the Android output callback.
|
||||
#[cfg(target_os = "android")]
|
||||
pub handler: AudioHandler<SessionAudioId>,
|
||||
/// Producer used by Android/macOS engine tasks to feed the output callback.
|
||||
#[cfg(any(target_os = "android", target_os = "macos"))]
|
||||
/// Producer used by Android engine tasks to feed the output callback.
|
||||
#[cfg(target_os = "android")]
|
||||
pub event_producer: crate::audio_event_queue::AudioEventProducer,
|
||||
/// AudioHandler that inbound decode+mix feeds into; the output
|
||||
/// callback pulls mixed stereo f32 from it.
|
||||
#[cfg(not(any(target_os = "android", target_os = "macos")))]
|
||||
/// callback pulls mixed stereo f32 from it. iOS, macOS, and desktop
|
||||
/// share this `Arc<Mutex<...>>` shape; the realtime callback uses
|
||||
/// `try_lock` so it never blocks on the tokio decode task (see
|
||||
/// `ios_voice_unit.rs` render callback).
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
||||
/// Master output gain (f32 bits stored in AtomicU32 for lock-free
|
||||
/// cross-thread read from the realtime audio callback).
|
||||
|
||||
Reference in New Issue
Block a user