feat(ios,p0): iOS P0 platform, audio fixes, channel UX
This commit is contained in:
@@ -85,8 +85,8 @@ use audiopus::{
|
||||
};
|
||||
use coreaudio::audio_unit::audio_format::LinearPcmFlags;
|
||||
use coreaudio::audio_unit::render_callback::{self, data};
|
||||
use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat};
|
||||
use coreaudio::audio_unit::IOType;
|
||||
use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tsclientlib::audio::AudioHandler;
|
||||
@@ -110,7 +110,6 @@ const FRAME_SAMPLES_MONO: usize = 960;
|
||||
/// shared `crate::framing` module.
|
||||
const MAX_OPUS_FRAME: usize = 1275;
|
||||
|
||||
|
||||
/// Sample rate every layer above us assumes. Matches the Opus
|
||||
/// encoder rate, the `tsclientlib::AudioHandler` mix rate, and the
|
||||
/// sample rate we ask iOS to give us via VPIO's StreamFormat.
|
||||
@@ -175,12 +174,9 @@ impl IosCaptureState {
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
mic_gain: f32,
|
||||
) -> Result<Self, AudioError> {
|
||||
let mut encoder = OpusEncoder::new(
|
||||
OpusSampleRate::Hz48000,
|
||||
OpusChannels::Mono,
|
||||
OpusApp::Voip,
|
||||
)
|
||||
.map_err(|e| AudioError::Opus(format!("encoder new (ios): {e}")))?;
|
||||
let mut encoder =
|
||||
OpusEncoder::new(OpusSampleRate::Hz48000, OpusChannels::Mono, OpusApp::Voip)
|
||||
.map_err(|e| AudioError::Opus(format!("encoder new (ios): {e}")))?;
|
||||
|
||||
// VoIP-tuned settings — bitrate 32 kbps, complexity 10,
|
||||
// inband FEC on, packet-loss-perc 5. Soft-fail each setter
|
||||
@@ -438,12 +434,8 @@ impl IosVoiceUnit {
|
||||
// scratch are owned by the closure — no Mutex needed
|
||||
// because the input callback is the sole writer/reader on
|
||||
// the audio thread.
|
||||
let mut capture_state = IosCaptureState::new(
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
frames_sent,
|
||||
mic_gain,
|
||||
)?;
|
||||
let mut capture_state =
|
||||
IosCaptureState::new(voice_out_tx, transmit_active, frames_sent, mic_gain)?;
|
||||
|
||||
unit.set_input_callback(move |args: render_callback::Args<data::Interleaved<i16>>| {
|
||||
// VPIO with our pinned stream format delivers
|
||||
@@ -530,10 +522,21 @@ impl IosVoiceUnit {
|
||||
// earlier callbacks (when scratch was bigger) would
|
||||
// leak through otherwise.
|
||||
scratch_stereo[..needed].fill(0.0);
|
||||
// Lock + fill. Same pattern as Linux/SDL output.
|
||||
{
|
||||
let mut h = handler_for_render.lock().unwrap();
|
||||
let _removed = h.fill_buffer(&mut scratch_stereo[..needed]);
|
||||
// Non-blocking fill on the realtime callback thread.
|
||||
// If the inbound forwarder currently owns this mutex,
|
||||
// emit this period as silence instead of blocking and
|
||||
// risking an AudioUnit underrun pop/click.
|
||||
match handler_for_render.try_lock() {
|
||||
Ok(mut h) => {
|
||||
let _removed = h.fill_buffer(&mut scratch_stereo[..needed]);
|
||||
}
|
||||
Err(std::sync::TryLockError::WouldBlock) => {
|
||||
// scratch_stereo is already zeroed above.
|
||||
}
|
||||
Err(std::sync::TryLockError::Poisoned(e)) => {
|
||||
// Never panic on the realtime IO thread.
|
||||
warn!(target: "chanora_audio", "AudioHandler mutex poisoned: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Downmix stereo f32 -> mono i16 with master gain.
|
||||
@@ -652,9 +655,43 @@ impl IosVoiceUnit {
|
||||
),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
unit,
|
||||
})
|
||||
Ok(Self { unit })
|
||||
}
|
||||
|
||||
/// Restart the audio unit after route change handling.
|
||||
///
|
||||
/// Route rebinding on iOS is most reliable when we bounce the
|
||||
/// VoiceProcessingIO unit through an uninitialize/reinitialize
|
||||
/// cycle, then start again.
|
||||
pub fn restart(&mut self) -> Result<(), AudioError> {
|
||||
self.unit
|
||||
.stop()
|
||||
.map_err(|e| AudioError::Backend(format!("vpio restart stop: {e}")))?;
|
||||
self.unit
|
||||
.uninitialize()
|
||||
.map_err(|e| AudioError::Backend(format!("vpio restart uninit: {e}")))?;
|
||||
self.unit
|
||||
.initialize()
|
||||
.map_err(|e| AudioError::Backend(format!("vpio restart init: {e}")))?;
|
||||
self.unit
|
||||
.start()
|
||||
.map_err(|e| AudioError::Backend(format!("vpio restart start: {e}")))?;
|
||||
info!(target: "chanora_audio", "ios VPIO audio unit restarted");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pause the audio unit during an interruption.
|
||||
pub fn pause(&mut self) -> Result<(), AudioError> {
|
||||
self.unit
|
||||
.stop()
|
||||
.map_err(|e| AudioError::Backend(format!("vpio pause stop: {e}")))
|
||||
}
|
||||
|
||||
/// Resume the audio unit after an interruption.
|
||||
pub fn resume(&mut self) -> Result<(), AudioError> {
|
||||
self.unit
|
||||
.start()
|
||||
.map_err(|e| AudioError::Backend(format!("vpio resume start: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user