chore: restore product scaffold to rollback baseline
This commit is contained in:
@@ -5,35 +5,28 @@
|
||||
//! and stopped before disconnect. It does not retry on device
|
||||
//! change.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
))]
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
))]
|
||||
use cpal::{SampleFormat, SizedSample};
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
))]
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
@@ -110,7 +103,6 @@ pub struct AudioDeviceInfo {
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
@@ -120,7 +112,6 @@ fn desktop_device_id(device: &cpal::Device) -> Option<String> {
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
@@ -132,7 +123,6 @@ fn short_device_id(id: &str) -> String {
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
@@ -188,53 +178,29 @@ pub fn list_audio_devices() -> AudioDeviceList {
|
||||
output_devices: Vec::new(),
|
||||
};
|
||||
let host = cpal::default_host();
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let default_input_name = host.default_input_device().and_then(|device| {
|
||||
device
|
||||
.description()
|
||||
.ok()
|
||||
.map(|description| description.name().trim().to_owned())
|
||||
.filter(|name| !name.is_empty())
|
||||
});
|
||||
let default_output_name = host.default_output_device().and_then(|device| {
|
||||
device
|
||||
.description()
|
||||
.ok()
|
||||
.map(|description| description.name().trim().to_owned())
|
||||
.filter(|name| !name.is_empty())
|
||||
});
|
||||
list.input_devices =
|
||||
crate::linux_pipewire_input::list_input_devices(default_input_name.as_deref());
|
||||
list.output_devices =
|
||||
crate::linux_pipewire_input::list_output_devices(default_output_name.as_deref());
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let default_in = host
|
||||
.default_input_device()
|
||||
.and_then(|device| desktop_device_id(&device));
|
||||
if let Ok(devices) = host.input_devices() {
|
||||
for d in devices {
|
||||
if let Some(mut device) = describe_device(&d) {
|
||||
device.is_default = default_in
|
||||
.as_ref()
|
||||
.is_some_and(|default_id| default_id == &device.id);
|
||||
list.input_devices.push(device);
|
||||
}
|
||||
let default_in = host
|
||||
.default_input_device()
|
||||
.and_then(|device| desktop_device_id(&device));
|
||||
let default_out = host
|
||||
.default_output_device()
|
||||
.and_then(|device| desktop_device_id(&device));
|
||||
if let Ok(devices) = host.input_devices() {
|
||||
for d in devices {
|
||||
if let Some(mut device) = describe_device(&d) {
|
||||
device.is_default = default_in
|
||||
.as_ref()
|
||||
.is_some_and(|default_id| default_id == &device.id);
|
||||
list.input_devices.push(device);
|
||||
}
|
||||
}
|
||||
let default_out = host
|
||||
.default_output_device()
|
||||
.and_then(|device| desktop_device_id(&device));
|
||||
if let Ok(devices) = host.output_devices() {
|
||||
for d in devices {
|
||||
if let Some(mut device) = describe_device(&d) {
|
||||
device.is_default = default_out
|
||||
.as_ref()
|
||||
.is_some_and(|default_id| default_id == &device.id);
|
||||
list.output_devices.push(device);
|
||||
}
|
||||
}
|
||||
if let Ok(devices) = host.output_devices() {
|
||||
for d in devices {
|
||||
if let Some(mut device) = describe_device(&d) {
|
||||
device.is_default = default_out
|
||||
.as_ref()
|
||||
.is_some_and(|default_id| default_id == &device.id);
|
||||
list.output_devices.push(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,23 +306,31 @@ pub struct AudioEngine {
|
||||
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
||||
client_volume_overrides: Arc<Mutex<HashMap<SessionAudioId, f32>>>,
|
||||
#[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))]
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
#[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))]
|
||||
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
||||
#[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))]
|
||||
mic_gain: f32,
|
||||
// Streams must be dropped to stop audio. Linux owns native
|
||||
// PipeWire/PulseAudio threads; Windows owns cpal streams. On iOS both
|
||||
// sides collapse into a single `IosVoiceUnit` (one VoiceProcessingIO
|
||||
// AudioUnit hosts mic + speaker).
|
||||
#[cfg(target_os = "linux")]
|
||||
_input_stream: Mutex<Option<crate::linux_pipewire_input::LinuxInput>>,
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
// Streams must be dropped to stop audio. Both are `!Send` because
|
||||
// cpal's Stream isn't Send on some backends; we keep them in an
|
||||
// Option wrapped by Mutex so stop() can move them out. On Linux
|
||||
// the output side is `crate::sdl_output::SdlOutput` instead of a
|
||||
// cpal Stream (see the SDD note inside `sdl_output.rs`); the
|
||||
// same unsafe Send/Sync impl below covers both. On iOS both
|
||||
// sides collapse into a single `IosVoiceUnit` (one
|
||||
// VoiceProcessingIO AudioUnit hosts mic + speaker) — cpal is
|
||||
// unused on iOS for the reasons documented in
|
||||
// `ios_voice_unit.rs`.
|
||||
#[cfg(all(
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
))]
|
||||
_input_stream: Mutex<Option<cpal::Stream>>,
|
||||
#[cfg(target_os = "linux")]
|
||||
_output_stream: Mutex<Option<crate::linux_pipewire_input::LinuxOutput>>,
|
||||
_output_stream: Mutex<Option<crate::sdl_output::SdlOutput>>,
|
||||
#[cfg(all(
|
||||
not(target_os = "linux"),
|
||||
not(target_os = "ios"),
|
||||
@@ -483,31 +457,6 @@ fn open_ios_voice_backend(
|
||||
Ok(IosVoiceBackend::Vpio(unit))
|
||||
}
|
||||
|
||||
fn apply_saved_client_volume(
|
||||
audio_handler: &mut AudioHandler<SessionAudioId>,
|
||||
client_volume_overrides: &Mutex<HashMap<SessionAudioId, f32>>,
|
||||
client_id: SessionAudioId,
|
||||
) {
|
||||
let override_volume = match client_volume_overrides.lock() {
|
||||
Ok(overrides) => overrides.get(&client_id).copied(),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
target: "chanora_audio",
|
||||
client_id = client_id.0,
|
||||
error = %error,
|
||||
"client volume overrides lock poisoned — saved volume not applied"
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(volume) = override_volume {
|
||||
if let Some(queue) = audio_handler.get_mut_queues().get_mut(&client_id) {
|
||||
queue.volume = volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEngine {
|
||||
#[cfg(target_os = "android")]
|
||||
fn spawn_android_backend_event_task(
|
||||
@@ -743,7 +692,7 @@ impl AudioEngine {
|
||||
Self::start_with_gate_cpal(cfg, voice_out_tx, voice_in_rx, transmit_gate)
|
||||
}
|
||||
|
||||
/// Non-Apple/non-Android implementation: native Linux voice I/O or cpal on Windows.
|
||||
/// Non-Apple/non-Android implementation: cpal capture + (cpal | SDL2) output.
|
||||
/// Kept as a separate function so the Apple and Android paths can
|
||||
/// short-circuit at the top of `start_with_gate` without dragging a 200-line
|
||||
/// cfg-gated block. Body is the pre-iOS-port code, unchanged
|
||||
@@ -762,16 +711,7 @@ impl AudioEngine {
|
||||
"starting audio engine: cpal host selected"
|
||||
);
|
||||
|
||||
fn device_name(device: &cpal::Device) -> Option<String> {
|
||||
device
|
||||
.description()
|
||||
.ok()
|
||||
.map(|description| description.name().trim().to_owned())
|
||||
.filter(|name| !name.is_empty())
|
||||
}
|
||||
|
||||
/// Helper: find a device by stable id, falling back to default.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn find_device<DefaultFn, AllFn, Devices>(
|
||||
host: &cpal::Host,
|
||||
default_fn: DefaultFn,
|
||||
@@ -795,30 +735,28 @@ impl AudioEngine {
|
||||
default_fn(host)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let in_dev = host.default_input_device();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let in_dev = find_device(
|
||||
&host,
|
||||
cpal::Host::default_input_device,
|
||||
cpal::Host::input_devices,
|
||||
cfg.input_device_id.as_deref(),
|
||||
);
|
||||
)
|
||||
.ok_or(AudioError::NoInputDevice)?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let out_dev = host.default_output_device();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let out_dev = find_device(
|
||||
&host,
|
||||
cpal::Host::default_output_device,
|
||||
cpal::Host::output_devices,
|
||||
cfg.output_device_id.as_deref(),
|
||||
);
|
||||
)
|
||||
.ok_or(AudioError::NoOutputDevice)?;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let in_dev = in_dev.ok_or(AudioError::NoInputDevice)?;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let out_dev = out_dev.ok_or(AudioError::NoOutputDevice)?;
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
in_device = %in_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||
out_device = %out_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||
"starting audio engine"
|
||||
);
|
||||
|
||||
// Log the cpal-reported default configs *before* trying to
|
||||
// open streams, so a downstream stream-build failure can
|
||||
@@ -830,29 +768,6 @@ impl AudioEngine {
|
||||
// "Start audio button not work"). Promote what would
|
||||
// otherwise be silent or laconic errors into structured
|
||||
// log records the user can paste back.
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(in_dev) = in_dev.as_ref() {
|
||||
match in_dev.default_input_config() {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate(),
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_input_config reported"
|
||||
),
|
||||
Err(e) => warn!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
"default_input_config FAILED — capture will be disabled"
|
||||
),
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
"cpal default_input_device unavailable; Linux PipeWire capture will be tried directly"
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
match in_dev.default_input_config() {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
@@ -867,24 +782,6 @@ impl AudioEngine {
|
||||
"default_input_config FAILED — capture will be disabled"
|
||||
),
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(out_dev) = out_dev.as_ref() {
|
||||
match out_dev.default_output_config() {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate(),
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_output_config reported"
|
||||
),
|
||||
Err(e) => warn!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
"default_output_config FAILED — output stream will fail to build"
|
||||
),
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
match out_dev.default_output_config() {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
@@ -900,7 +797,6 @@ impl AudioEngine {
|
||||
),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let transmit_flag_for_capture = transmit_gate.flag_arc();
|
||||
let frames_sent = Arc::new(AtomicU32::new(0));
|
||||
let frames_received = Arc::new(AtomicU32::new(0));
|
||||
@@ -908,7 +804,6 @@ impl AudioEngine {
|
||||
let output_muted = Arc::new(AtomicBool::new(false));
|
||||
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
|
||||
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
|
||||
let client_volume_overrides = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
// ---------- Capture ----------
|
||||
// Capture is best-effort. If the platform default input
|
||||
@@ -916,52 +811,24 @@ impl AudioEngine {
|
||||
// headless null sources or for users who deny the mic
|
||||
// permission) we log and continue — playback alone is
|
||||
// still useful. PTT becomes a no-op in that case.
|
||||
#[cfg(target_os = "linux")]
|
||||
let capture_result = match crate::linux_pipewire_input::start_capture(
|
||||
cfg.input_device_id.as_deref(),
|
||||
in_dev.as_ref().and_then(device_name).as_deref(),
|
||||
voice_out_tx.clone(),
|
||||
transmit_gate.clone(),
|
||||
frames_sent.clone(),
|
||||
cfg.mic_gain,
|
||||
) {
|
||||
Ok((stream, device_info)) => Ok((Some(stream), true, device_info.name)),
|
||||
Err(linux_error) => {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
error = %linux_error,
|
||||
"Linux native capture unavailable"
|
||||
);
|
||||
Err(linux_error)
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let capture_result = try_open_capture(
|
||||
&in_dev,
|
||||
voice_out_tx,
|
||||
transmit_flag_for_capture,
|
||||
frames_sent.clone(),
|
||||
cfg.mic_gain,
|
||||
)
|
||||
.map(|stream| {
|
||||
(
|
||||
Some(stream),
|
||||
true,
|
||||
device_name(&in_dev).unwrap_or_else(|| "cpal capture".to_string()),
|
||||
)
|
||||
});
|
||||
let (input_stream, capture_active, capture_device_name) = match capture_result {
|
||||
Ok((stream, active, device_name)) => (stream, active, device_name),
|
||||
);
|
||||
let (input_stream, capture_active) = match capture_result {
|
||||
Ok(s) => (Some(s), true),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
"capture stream unavailable; continuing with playback only"
|
||||
);
|
||||
(None, false, "<capture unavailable>".to_string())
|
||||
(None, false)
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(s) = &input_stream {
|
||||
s.play()
|
||||
.map_err(|e| AudioError::Backend(format!("input play: {e}")))?;
|
||||
@@ -971,18 +838,21 @@ impl AudioEngine {
|
||||
let audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>> =
|
||||
Arc::new(Mutex::new(AudioHandler::new()));
|
||||
|
||||
// Linux uses native PipeWire/PulseAudio playback. PipeWire is primary;
|
||||
// PulseAudio is fallback and can also be selected explicitly by id.
|
||||
// Linux uses SDL2 for output (Qint / upstream tsclientlib
|
||||
// pattern). cpal's Linux backend opens raw ALSA which routes
|
||||
// through `dmix`+`plug` and produces audible crackling /
|
||||
// popping on the 48 kHz → device-rate step. SDL2 on the same
|
||||
// box routes through PipeWire's PA bridge (or PulseAudio)
|
||||
// whose resampler is high-quality. We keep cpal on Windows
|
||||
// and macOS — both have native backends (WASAPI / CoreAudio)
|
||||
// without this problem. See `crates/chanora_audio/src/sdl_output.rs`
|
||||
// for the full rationale.
|
||||
#[cfg(target_os = "linux")]
|
||||
let (output_stream, output_device_info) = crate::linux_pipewire_input::start_output(
|
||||
cfg.output_device_id.as_deref(),
|
||||
out_dev.as_ref().and_then(device_name).as_deref(),
|
||||
let output_stream = crate::sdl_output::SdlOutput::start(
|
||||
audio_handler.clone(),
|
||||
output_gain.clone(),
|
||||
output_muted.clone(),
|
||||
)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
let output_device_name = output_device_info.name;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let output_stream = {
|
||||
@@ -1057,20 +927,10 @@ impl AudioEngine {
|
||||
.map_err(|e| AudioError::Backend(format!("output play: {e}")))?;
|
||||
stream
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let output_device_name = device_name(&out_dev).unwrap_or_default();
|
||||
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
in_device = %capture_device_name,
|
||||
out_device = %output_device_name,
|
||||
"starting audio engine"
|
||||
);
|
||||
|
||||
// ---------- Inbound forwarder ----------
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let handler_for_task = audio_handler.clone();
|
||||
let client_volume_overrides_for_task = client_volume_overrides.clone();
|
||||
let frames_received_for_task = frames_received.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -1087,11 +947,6 @@ impl AudioEngine {
|
||||
if let Err(e) = h.handle_packet(id, v.packet) {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
} else {
|
||||
apply_saved_client_volume(
|
||||
&mut h,
|
||||
&client_volume_overrides_for_task,
|
||||
id,
|
||||
);
|
||||
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -1111,10 +966,6 @@ impl AudioEngine {
|
||||
audio_processing_config,
|
||||
audio_processing_stats,
|
||||
audio_handler,
|
||||
client_volume_overrides,
|
||||
#[cfg(target_os = "linux")]
|
||||
_input_stream: Mutex::new(input_stream),
|
||||
#[cfg(target_os = "windows")]
|
||||
_input_stream: Mutex::new(input_stream),
|
||||
_output_stream: Mutex::new(Some(output_stream)),
|
||||
shutdown_tx: Some(shutdown_tx),
|
||||
@@ -1140,7 +991,6 @@ impl AudioEngine {
|
||||
let output_muted = Arc::new(AtomicBool::new(false));
|
||||
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
|
||||
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
|
||||
let client_volume_overrides = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>> =
|
||||
Arc::new(Mutex::new(AudioHandler::new()));
|
||||
@@ -1287,7 +1137,6 @@ impl AudioEngine {
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let handler_for_task = audio_handler.clone();
|
||||
let client_volume_overrides_for_task = client_volume_overrides.clone();
|
||||
let frames_received_for_task = frames_received.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -1304,11 +1153,6 @@ impl AudioEngine {
|
||||
if let Err(e) = h.handle_packet(id, v.packet) {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
} else {
|
||||
apply_saved_client_volume(
|
||||
&mut h,
|
||||
&client_volume_overrides_for_task,
|
||||
id,
|
||||
);
|
||||
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -1328,7 +1172,6 @@ impl AudioEngine {
|
||||
audio_processing_config,
|
||||
audio_processing_stats,
|
||||
audio_handler,
|
||||
client_volume_overrides,
|
||||
voice_out_tx,
|
||||
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
||||
mic_gain: cfg.mic_gain,
|
||||
@@ -1381,7 +1224,6 @@ impl AudioEngine {
|
||||
let output_muted = Arc::new(AtomicBool::new(false));
|
||||
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
|
||||
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
|
||||
let client_volume_overrides = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>> =
|
||||
Arc::new(Mutex::new(AudioHandler::new()));
|
||||
@@ -1416,7 +1258,6 @@ impl AudioEngine {
|
||||
// waiting.
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let handler_for_task = audio_handler.clone();
|
||||
let client_volume_overrides_for_task = client_volume_overrides.clone();
|
||||
let frames_received_for_task = frames_received.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -1433,11 +1274,6 @@ impl AudioEngine {
|
||||
if let Err(e) = h.handle_packet(id, v.packet) {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
} else {
|
||||
apply_saved_client_volume(
|
||||
&mut h,
|
||||
&client_volume_overrides_for_task,
|
||||
id,
|
||||
);
|
||||
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -1457,7 +1293,6 @@ impl AudioEngine {
|
||||
audio_processing_config,
|
||||
audio_processing_stats,
|
||||
audio_handler,
|
||||
client_volume_overrides,
|
||||
voice_out_tx,
|
||||
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
||||
mic_gain: cfg.mic_gain,
|
||||
@@ -1477,8 +1312,12 @@ impl AudioEngine {
|
||||
// common contract is that dropping the wrapper stops
|
||||
// audio. iOS collapses input + output into one
|
||||
// `IosVoiceUnit` (see `ios_voice_unit.rs`); every other
|
||||
// platform has separate input + output stream owners.
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
// platform has separate cpal input + cpal/SDL output.
|
||||
#[cfg(all(
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "android")
|
||||
))]
|
||||
{
|
||||
let _ = self._input_stream.lock().unwrap().take();
|
||||
let _ = self._output_stream.lock().unwrap().take();
|
||||
@@ -1774,11 +1613,11 @@ impl AudioEngine {
|
||||
}
|
||||
|
||||
/// Latest Android voice-audio diagnostics snapshot (SDD-112 item
|
||||
/// 10 / SDD-113 item 7 / SDD-116 item 3). Returns `Some(...)`
|
||||
/// 10 / SDD-113 item 7 / SDD-116 item 3). On non-Android targets
|
||||
/// this always returns `None`. On Android it returns `Some(...)`
|
||||
/// once `AndroidVoiceUnit::open()` has published a snapshot; the
|
||||
/// slot is cleared on `close()` / `Drop`. Per SDD-090 the snapshot
|
||||
/// contains only device-side technical scalars — no PII.
|
||||
#[cfg(target_os = "android")]
|
||||
/// slot is cleared on `close()` / `Drop`. Per SDD-090 the
|
||||
/// snapshot contains only device-side technical scalars — no PII.
|
||||
pub fn android_diagnostics(
|
||||
&self,
|
||||
) -> Option<crate::mobile_voice_backend::AndroidAudioDiagnostics> {
|
||||
@@ -1830,25 +1669,6 @@ impl AudioEngine {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match self.client_volume_overrides.lock() {
|
||||
Ok(mut overrides) => {
|
||||
if (clamped - 1.0).abs() <= f32::EPSILON {
|
||||
overrides.remove(&SessionAudioId(client_id));
|
||||
} else {
|
||||
overrides.insert(SessionAudioId(client_id), clamped);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
target: "chanora_audio",
|
||||
client_id,
|
||||
volume = clamped,
|
||||
error = %e,
|
||||
"set_client_volume: client volume overrides lock poisoned"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1860,7 +1680,7 @@ impl Drop for AudioEngine {
|
||||
|
||||
// ---------- Capture pipeline ----------
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
fn try_open_capture(
|
||||
in_dev: &cpal::Device,
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
@@ -1879,8 +1699,9 @@ fn try_open_capture(
|
||||
// jitter of WASAPI shared mode.
|
||||
// * macOS / iOS: CoreAudio picks a HAL-friendly default;
|
||||
// iOS RemoteIO rejects arbitrary buffer-size requests.
|
||||
// * Linux does not reach this cpal capture path in production;
|
||||
// it uses native PipeWire/PulseAudio capture instead.
|
||||
// * Linux: same SDL2-vs-cpal split as the output path; we
|
||||
// still use cpal for capture but leave Default since
|
||||
// PipeWire's ALSA shim works well there.
|
||||
let mut in_stream_cfg: cpal::StreamConfig = in_cfg.into();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -1917,7 +1738,7 @@ fn try_open_capture(
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
pub(crate) struct CaptureState {
|
||||
struct CaptureState {
|
||||
encoder: OpusEncoder,
|
||||
in_sample_rate: u32,
|
||||
in_channels: usize,
|
||||
@@ -1956,7 +1777,7 @@ pub(crate) struct CaptureState {
|
||||
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
impl CaptureState {
|
||||
pub(crate) fn new(
|
||||
fn new(
|
||||
encoder: OpusEncoder,
|
||||
in_sample_rate: u32,
|
||||
in_channels: usize,
|
||||
@@ -1989,7 +1810,7 @@ impl CaptureState {
|
||||
/// Consume an arbitrary-rate, multichannel cpal buffer; produce
|
||||
/// 48 kHz mono frames; encode and send when `transmit_active`
|
||||
/// is true (PTT engaged).
|
||||
pub(crate) fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
|
||||
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
|
||||
if !self.transmit_active.load(Ordering::Relaxed) {
|
||||
// Drain accumulator while muted so we don't pop on PTT release.
|
||||
self.pcm_accum.clear();
|
||||
@@ -2138,7 +1959,7 @@ impl CaptureState {
|
||||
|
||||
/// Per-sample format conversion to f32 in the range [-1.0, 1.0].
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
pub(crate) trait ToF32 {
|
||||
trait ToF32 {
|
||||
fn to_f32_sample(self) -> f32;
|
||||
}
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
@@ -2160,7 +1981,7 @@ impl ToF32 for u16 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
fn build_input_stream<T>(
|
||||
device: &cpal::Device,
|
||||
config: &cpal::StreamConfig,
|
||||
|
||||
Reference in New Issue
Block a user