feat: integrate chat voice and diagnostics client
This commit is contained in:
@@ -44,9 +44,8 @@ mod inner {
|
||||
use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, warn};
|
||||
use tsclientlib::audio::AudioHandler;
|
||||
|
||||
use crate::engine::SessionAudioId;
|
||||
use crate::mobile_voice_backend::VoiceAudioParams;
|
||||
use crate::processor::AudioProcessor;
|
||||
use crate::AudioError;
|
||||
use chanora_protocol::OutPacket;
|
||||
@@ -121,6 +120,7 @@ mod inner {
|
||||
ten_vad_worker: Option<crate::vad::TenOnnxVadWorker>,
|
||||
current_vad_backend: crate::VadBackend,
|
||||
silero_model_epoch: u64,
|
||||
ten_model_epoch: u64,
|
||||
capture_frame_seq: u64,
|
||||
vad_state: crate::voice_activity::VoiceActivityStateMachine,
|
||||
/// Processing config — retained for route-change reloads.
|
||||
@@ -136,18 +136,12 @@ mod inner {
|
||||
|
||||
impl RawCaptureState {
|
||||
fn new(
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
transmit_active: Arc<AtomicBool>,
|
||||
output_muted: Arc<AtomicBool>,
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
mic_gain: f32,
|
||||
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
||||
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
params: &VoiceAudioParams,
|
||||
render_reference: Arc<RenderReferenceBuffer>,
|
||||
) -> Result<Self, AudioError> {
|
||||
let encoder = crate::opus_voice::new_voip_encoder("ios raw")?;
|
||||
let webrtc_apm_config = audio_processing_config
|
||||
let webrtc_apm_config = params
|
||||
.audio_processing_config
|
||||
.lock()
|
||||
.map(|cfg| crate::processor::webrtc_apm::WebRtcApmConfig::from_audio_config(&cfg))
|
||||
.unwrap_or_default();
|
||||
@@ -155,24 +149,25 @@ mod inner {
|
||||
encoder,
|
||||
pcm_accum: Vec::with_capacity(crate::frame::FRAME_20MS_SAMPLES * 2),
|
||||
opus_out: [0u8; crate::opus_voice::MAX_OPUS_FRAME],
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
output_muted,
|
||||
frames_sent,
|
||||
mic_gain,
|
||||
voice_activity_selector,
|
||||
voice_out_tx: params.voice_out_tx.clone(),
|
||||
transmit_active: params.transmit_active.clone(),
|
||||
output_muted: params.output_muted.clone(),
|
||||
frames_sent: params.frames_sent.clone(),
|
||||
mic_gain: params.mic_gain,
|
||||
voice_activity_selector: params.voice_activity_selector.clone(),
|
||||
vad_detector: crate::vad::WebRtcFallbackVad::default(),
|
||||
silero_vad_worker: None,
|
||||
ten_vad_worker: None,
|
||||
current_vad_backend: crate::VadBackend::WebrtcVad,
|
||||
silero_model_epoch: crate::vad::silero_model_epoch(),
|
||||
ten_model_epoch: crate::vad::ten_model_epoch(),
|
||||
capture_frame_seq: 0,
|
||||
vad_state: crate::voice_activity::VoiceActivityStateMachine::default(),
|
||||
audio_processing_config,
|
||||
audio_processing_config: params.audio_processing_config.clone(),
|
||||
webrtc_apm_processor: crate::processor::WebRtcApmProcessor::with_config(
|
||||
webrtc_apm_config,
|
||||
)?,
|
||||
audio_processing_stats,
|
||||
audio_processing_stats: params.audio_processing_stats.clone(),
|
||||
render_reference,
|
||||
pending_10ms: [0_i16; crate::frame::FRAME_10MS_SAMPLES],
|
||||
pending_10ms_len: 0,
|
||||
@@ -184,11 +179,20 @@ mod inner {
|
||||
fn mark_vad_fallback_active(&mut self, failed_backend: crate::VadBackend) {
|
||||
if self.fallback_warned_backend != Some(failed_backend) {
|
||||
self.fallback_warned_backend = Some(failed_backend);
|
||||
tracing::warn!(
|
||||
target: "chanora_audio",
|
||||
backend = failed_backend.as_str(),
|
||||
"VAD backend unavailable; using WebRTC fallback for runtime detection"
|
||||
);
|
||||
if self.capture_frame_seq < 128 {
|
||||
tracing::info!(
|
||||
target: "chanora_audio",
|
||||
backend = failed_backend.as_str(),
|
||||
seq = self.capture_frame_seq,
|
||||
"VAD backend warming up; using WebRTC fallback"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
target: "chanora_audio",
|
||||
backend = failed_backend.as_str(),
|
||||
"VAD backend unavailable; using WebRTC fallback for runtime detection"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,11 +290,15 @@ mod inner {
|
||||
|
||||
// Switch VAD backend when config changes.
|
||||
let silero_epoch = crate::vad::silero_model_epoch();
|
||||
let ten_epoch = crate::vad::ten_model_epoch();
|
||||
let silero_changed = vad_backend == crate::VadBackend::SileroOnnx
|
||||
&& silero_epoch != self.silero_model_epoch;
|
||||
if vad_backend != self.current_vad_backend || silero_changed {
|
||||
let ten_changed =
|
||||
vad_backend == crate::VadBackend::TenVad && ten_epoch != self.ten_model_epoch;
|
||||
if vad_backend != self.current_vad_backend || silero_changed || ten_changed {
|
||||
self.current_vad_backend = vad_backend;
|
||||
self.silero_model_epoch = silero_epoch;
|
||||
self.ten_model_epoch = ten_epoch;
|
||||
self.fallback_warned_backend = None;
|
||||
match vad_backend {
|
||||
crate::VadBackend::SileroOnnx => {
|
||||
@@ -335,12 +343,17 @@ mod inner {
|
||||
} else if vad_backend == crate::VadBackend::SileroOnnx {
|
||||
if let Some(worker) = self.silero_vad_worker.as_ref() {
|
||||
let enqueued = worker.try_send(capture_seq, &frame);
|
||||
if enqueued && !worker.is_stale(capture_seq) {
|
||||
if !worker.is_stale(capture_seq) {
|
||||
let p = worker.latest_probability();
|
||||
crate::vad::VadOutput {
|
||||
probability: p,
|
||||
speech: p >= 0.5,
|
||||
}
|
||||
} else if enqueued {
|
||||
crate::vad::VadOutput {
|
||||
probability: 0.0,
|
||||
speech: false,
|
||||
}
|
||||
} else {
|
||||
used_fallback_vad = true;
|
||||
self.mark_vad_fallback_active(vad_backend);
|
||||
@@ -357,12 +370,17 @@ mod inner {
|
||||
} else if vad_backend == crate::VadBackend::TenVad {
|
||||
if let Some(worker) = self.ten_vad_worker.as_ref() {
|
||||
let enqueued = worker.try_send(capture_seq, &frame);
|
||||
if enqueued && !worker.is_stale(capture_seq) {
|
||||
if !worker.is_stale(capture_seq) {
|
||||
let p = worker.latest_probability();
|
||||
crate::vad::VadOutput {
|
||||
probability: p,
|
||||
speech: p >= 0.5,
|
||||
}
|
||||
} else if enqueued {
|
||||
crate::vad::VadOutput {
|
||||
probability: 0.0,
|
||||
speech: false,
|
||||
}
|
||||
} else {
|
||||
used_fallback_vad = true;
|
||||
self.mark_vad_fallback_active(vad_backend);
|
||||
@@ -422,22 +440,10 @@ mod inner {
|
||||
|
||||
impl IosRawUnit {
|
||||
/// Open a RemoteIO AudioUnit, install render + input callbacks, start.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn start(
|
||||
handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
||||
output_gain: Arc<AtomicU32>,
|
||||
output_muted: Arc<AtomicBool>,
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
transmit_active: Arc<AtomicBool>,
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
mic_gain: f32,
|
||||
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
||||
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
) -> Result<Self, AudioError> {
|
||||
pub(crate) fn start(params: VoiceAudioParams) -> Result<Self, AudioError> {
|
||||
// INV_010: reject if config requests VPIO (that's IosVoiceUnit's job).
|
||||
{
|
||||
let cfg = audio_processing_config.lock().unwrap();
|
||||
let cfg = params.audio_processing_config.lock().unwrap();
|
||||
if cfg.ios_mode == crate::IosVoiceProcessingMode::PlatformVoiceProcessing {
|
||||
return Err(AudioError::InvalidAudioProcessingConfig(
|
||||
"IosRawUnit requires raw WebRTC APM mode".to_string(),
|
||||
@@ -470,17 +476,7 @@ mod inner {
|
||||
let render_ref_buf = RenderReferenceBuffer::new();
|
||||
let render_ref_for_capture = render_ref_buf.clone();
|
||||
|
||||
let mut capture_state = RawCaptureState::new(
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
output_muted.clone(),
|
||||
frames_sent,
|
||||
mic_gain,
|
||||
voice_activity_selector,
|
||||
audio_processing_config,
|
||||
audio_processing_stats.clone(),
|
||||
render_ref_for_capture,
|
||||
)?;
|
||||
let mut capture_state = RawCaptureState::new(¶ms, render_ref_for_capture)?;
|
||||
|
||||
unit.set_input_callback(move |args: render_callback::Args<data::Interleaved<i16>>| {
|
||||
capture_state.ingest_i16(args.data.buffer);
|
||||
@@ -489,7 +485,10 @@ mod inner {
|
||||
.map_err(|e| AudioError::Backend(format!("remoteio input cb: {e}")))?;
|
||||
|
||||
let mut scratch: Vec<f32> = Vec::with_capacity(2048);
|
||||
let stats_render = audio_processing_stats.clone();
|
||||
let handler = params.handler.clone();
|
||||
let output_gain = params.output_gain.clone();
|
||||
let output_muted = params.output_muted.clone();
|
||||
let stats_render = params.audio_processing_stats.clone();
|
||||
|
||||
unit.set_render_callback(move |args: render_callback::Args<data::Interleaved<i16>>| {
|
||||
let out = args.data.buffer;
|
||||
|
||||
Reference in New Issue
Block a user