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:
Edison Jwa
2026-06-07 23:27:46 +09:00
parent eb10db5b59
commit d59da05f93
5 changed files with 101 additions and 119 deletions
+13 -33
View File
@@ -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) => {
+2 -1
View File
@@ -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).