fix(audio): eliminate Android output stutter via Oboe config + lock-free callback (#20)
* fix(audio): eliminate Android output stutter via Oboe config + lock-free callback Phase 1 — Oboe configuration: - Change output stream from Usage::VoiceCommunication to Usage::Game with ContentType::Sonification to avoid forcing the Legacy (OpenSL ES) data path on most devices (Oboe issue #2075) - Switch output format from i16 Mono to f32 Stereo, matching Qint's proven configuration and eliminating per-callback downmix conversion - Set buffer size to 2x burst after stream open, reducing default buffer from 8-20x burst to 2x burst for lower latency - Remove scratch Mutex<Vec<f32>>; callback writes directly to Oboe buffer Phase 2 — Lock-free output callback: - Add audio_event_queue.rs: lock-free SPSC bridge using crossbeam ArrayQueue with separate packet (lossy) and control (reliable) channels - OutputCallback now owns AudioHandler directly (no Arc<Mutex<>> on Android) - Inbound forwarder pushes packets via AudioEventProducer (no mutex) - set_client_volume pushes control commands via event queue on Android - iOS/desktop Arc<Mutex<AudioHandler>> path unchanged * fix(audio): address PR #20 review findings - Store AudioEventConsumer directly in OutputCallback to eliminate per-callback Arc clone on the real-time audio thread - Add SAFETY comment for the unsafe from_raw_parts_mut transmute - Bound set_client_volume spin-loop to 64 retries with warn log - Remove redundant crossbeam-utils direct dependency - Regenerate license inventory for new crossbeam deps (CI fix) * fix(audio): use ASCII TODO punctuation
This commit is contained in:
Generated
+21
@@ -423,6 +423,7 @@ dependencies = [
|
|||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"cpal",
|
"cpal",
|
||||||
"criterion",
|
"criterion",
|
||||||
|
"crossbeam",
|
||||||
"dhat",
|
"dhat",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -803,6 +804,17 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-queue",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.15"
|
version = "0.5.15"
|
||||||
@@ -831,6 +843,15 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-queue"
|
||||||
|
version = "0.3.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ audiopus = "0.3.0-rc.0"
|
|||||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||||
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||||
rustfft = "6.2.0"
|
rustfft = "6.2.0"
|
||||||
|
crossbeam = { version = "0.8", default-features = false, features = ["alloc", "crossbeam-queue"] }
|
||||||
|
|
||||||
[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies]
|
[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies]
|
||||||
# Desktop audio I/O for Windows capture/playback and Linux capture.
|
# Desktop audio I/O for Windows capture/playback and Linux capture.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use audiopus::coder::Encoder as OpusEncoder;
|
use audiopus::coder::Encoder as OpusEncoder;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::audio_event_queue::{AudioCommand, AudioEventQueue};
|
||||||
use crate::mobile_voice_backend::{
|
use crate::mobile_voice_backend::{
|
||||||
clear_android_audio_diagnostics, latency_tier_for, next_input_preset_after,
|
clear_android_audio_diagnostics, latency_tier_for, next_input_preset_after,
|
||||||
next_sharing_mode_after, publish_android_audio_diagnostics, AchievedInputPreset,
|
next_sharing_mode_after, publish_android_audio_diagnostics, AchievedInputPreset,
|
||||||
@@ -63,7 +64,7 @@ use oboe::{
|
|||||||
AudioInputCallback, AudioInputStreamSafe, AudioOutputCallback, AudioOutputStreamSafe,
|
AudioInputCallback, AudioInputStreamSafe, AudioOutputCallback, AudioOutputStreamSafe,
|
||||||
AudioStream, AudioStreamAsync, AudioStreamBase, AudioStreamBuilder, AudioStreamSafe,
|
AudioStream, AudioStreamAsync, AudioStreamBase, AudioStreamBuilder, AudioStreamSafe,
|
||||||
DataCallbackResult, Input as OboeInput, InputPreset, Mono, Output as OboeOutput,
|
DataCallbackResult, Input as OboeInput, InputPreset, Mono, Output as OboeOutput,
|
||||||
PerformanceMode, SessionId, SharingMode, Usage,
|
PerformanceMode, SessionId, SharingMode, Stereo, Usage,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::processor::AudioProcessor;
|
use crate::processor::AudioProcessor;
|
||||||
@@ -518,14 +519,14 @@ impl AudioInputCallback for InputCallback {
|
|||||||
//
|
//
|
||||||
// Mirrors the iOS VPIO render callback. Pulls mixed 48 kHz stereo f32
|
// Mirrors the iOS VPIO render callback. Pulls mixed 48 kHz stereo f32
|
||||||
// from `AudioHandler::fill_buffer`, applies output gain + mute, and
|
// from `AudioHandler::fill_buffer`, applies output gain + mute, and
|
||||||
// writes mono i16 to the Oboe output buffer.
|
// writes stereo f32 directly to the Oboe output buffer.
|
||||||
|
|
||||||
struct OutputCallback {
|
struct OutputCallback {
|
||||||
handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
handler: AudioHandler<SessionAudioId>,
|
||||||
|
event_consumer: crate::audio_event_queue::AudioEventConsumer,
|
||||||
output_gain: Arc<AtomicU32>,
|
output_gain: Arc<AtomicU32>,
|
||||||
output_muted: Arc<AtomicBool>,
|
output_muted: Arc<AtomicBool>,
|
||||||
event_tx: BackendEventTx,
|
event_tx: BackendEventTx,
|
||||||
scratch: Arc<Mutex<Vec<f32>>>,
|
|
||||||
render_reference: Arc<RenderReferenceBuffer>,
|
render_reference: Arc<RenderReferenceBuffer>,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||||
pending_render_ref: [f32; crate::frame::FRAME_10MS_SAMPLES],
|
pending_render_ref: [f32; crate::frame::FRAME_10MS_SAMPLES],
|
||||||
@@ -533,51 +534,65 @@ struct OutputCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AudioOutputCallback for OutputCallback {
|
impl AudioOutputCallback for OutputCallback {
|
||||||
type FrameType = (i16, Mono);
|
type FrameType = (f32, Stereo);
|
||||||
|
|
||||||
fn on_audio_ready(
|
fn on_audio_ready(
|
||||||
&mut self,
|
&mut self,
|
||||||
_stream: &mut dyn AudioOutputStreamSafe,
|
_stream: &mut dyn AudioOutputStreamSafe,
|
||||||
frames: &mut [i16],
|
frames: &mut [(f32, f32)],
|
||||||
) -> DataCallbackResult {
|
) -> DataCallbackResult {
|
||||||
let _ = catch_unwind(AssertUnwindSafe(|| {
|
let _ = catch_unwind(AssertUnwindSafe(|| {
|
||||||
let needed = frames.len() * 2; // stereo
|
// SAFETY: `frames: &mut [(f32, f32)]` is an interleaved stereo
|
||||||
let scratch = &mut self.scratch.lock().unwrap();
|
// buffer. `(f32, f32)` has the same size (8 bytes) and alignment
|
||||||
if scratch.len() < needed {
|
// (4 bytes) as `[f32; 2]`, so reinterpreting the slice as a flat
|
||||||
scratch.resize(needed, 0.0);
|
// `&mut [f32]` of length `frames.len() * 2` is sound. The Rust
|
||||||
} else {
|
// reference does not *guarantee* `#[repr(Rust)]` tuple layout,
|
||||||
for s in &mut scratch[..needed] {
|
// but (a) both fields are identical F32 primitives with no
|
||||||
*s = 0.0;
|
// padding possible, and (b) the oboe crate uses
|
||||||
|
// `#[repr(transparent)]` on its frame type alias so the ABI
|
||||||
|
// contract is upheld at the FFI boundary. `frames` is not
|
||||||
|
// accessed again after `buf` is created, so no aliasing UB.
|
||||||
|
let buf: &mut [f32] = unsafe {
|
||||||
|
std::slice::from_raw_parts_mut(frames.as_mut_ptr() as *mut f32, frames.len() * 2)
|
||||||
|
};
|
||||||
|
for s in buf.iter_mut() {
|
||||||
|
*s = 0.0;
|
||||||
|
}
|
||||||
|
for cmd in self.event_consumer.drain_controls() {
|
||||||
|
match cmd {
|
||||||
|
AudioCommand::SetVolume(id, vol) => {
|
||||||
|
if let Some(q) = self.handler.get_mut_queues().get_mut(&id) {
|
||||||
|
q.volume = vol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioCommand::RemoveClient(id) => {
|
||||||
|
self.handler.get_mut_queues().remove(&id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match self.handler.try_lock() {
|
|
||||||
Ok(mut h) => {
|
for pkt in self.event_consumer.drain_packets(50) {
|
||||||
let _ = h.fill_buffer(&mut scratch[..needed]);
|
if let Err(e) = self.handler.handle_packet(pkt.client_id, pkt.data) {
|
||||||
}
|
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||||
Err(std::sync::TryLockError::WouldBlock) => {}
|
|
||||||
Err(std::sync::TryLockError::Poisoned(e)) => {
|
|
||||||
warn!(
|
|
||||||
target: "chanora_audio",
|
|
||||||
"AudioHandler mutex poisoned: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = self.handler.fill_buffer(buf);
|
||||||
let gain = f32::from_bits(self.output_gain.load(Ordering::Relaxed));
|
let gain = f32::from_bits(self.output_gain.load(Ordering::Relaxed));
|
||||||
let muted = self.output_muted.load(Ordering::Relaxed);
|
let muted = self.output_muted.load(Ordering::Relaxed);
|
||||||
let _ = crate::voice_render::downmix_stereo_f32_to_mono_i16(
|
if muted {
|
||||||
&scratch[..needed],
|
for s in buf.iter_mut() {
|
||||||
frames,
|
*s = 0.0;
|
||||||
gain,
|
}
|
||||||
muted,
|
} else if gain != 1.0 {
|
||||||
);
|
for s in buf.iter_mut() {
|
||||||
|
*s *= gain;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.audio_processing_stats
|
self.audio_processing_stats
|
||||||
.update_render(crate::frame::dbfs(&scratch[..needed]), frames.len() as u32);
|
.update_render(crate::frame::dbfs(buf), frames.len() as u32);
|
||||||
|
|
||||||
// Accumulate the full render callback into 10 ms mono chunks so
|
for chunk in buf.chunks_exact(2) {
|
||||||
// AEC sees consistent reference timing even when output callbacks
|
|
||||||
// are shorter or longer than 10 ms.
|
|
||||||
for chunk in scratch[..needed].chunks_exact(2) {
|
|
||||||
self.pending_render_ref[self.pending_render_ref_len] = (chunk[0] + chunk[1]) * 0.5;
|
self.pending_render_ref[self.pending_render_ref_len] = (chunk[0] + chunk[1]) * 0.5;
|
||||||
self.pending_render_ref_len += 1;
|
self.pending_render_ref_len += 1;
|
||||||
if self.pending_render_ref_len == crate::frame::FRAME_10MS_SAMPLES {
|
if self.pending_render_ref_len == crate::frame::FRAME_10MS_SAMPLES {
|
||||||
@@ -677,8 +692,6 @@ impl AndroidVoiceUnit {
|
|||||||
.map_err(|e| BackendError::OpenFailed(format!("capture state init: {e}")))?,
|
.map_err(|e| BackendError::OpenFailed(format!("capture state init: {e}")))?,
|
||||||
));
|
));
|
||||||
|
|
||||||
let scratch = Arc::new(Mutex::new(Vec::with_capacity(8192)));
|
|
||||||
|
|
||||||
// --- Open input stream (SDD-112) ---------------------------
|
// --- Open input stream (SDD-112) ---------------------------
|
||||||
let input_builder = AudioStreamBuilder::default()
|
let input_builder = AudioStreamBuilder::default()
|
||||||
.set_direction::<OboeInput>()
|
.set_direction::<OboeInput>()
|
||||||
@@ -766,8 +779,8 @@ impl AndroidVoiceUnit {
|
|||||||
let output_builder = AudioStreamBuilder::default()
|
let output_builder = AudioStreamBuilder::default()
|
||||||
.set_direction::<OboeOutput>()
|
.set_direction::<OboeOutput>()
|
||||||
.set_sample_rate(cfg.sample_rate as i32)
|
.set_sample_rate(cfg.sample_rate as i32)
|
||||||
.set_channel_count::<Mono>()
|
.set_channel_count::<Stereo>()
|
||||||
.set_format::<i16>()
|
.set_format::<f32>()
|
||||||
.set_performance_mode(if cfg.request_low_latency {
|
.set_performance_mode(if cfg.request_low_latency {
|
||||||
PerformanceMode::LowLatency
|
PerformanceMode::LowLatency
|
||||||
} else {
|
} else {
|
||||||
@@ -778,16 +791,21 @@ impl AndroidVoiceUnit {
|
|||||||
} else {
|
} else {
|
||||||
SharingMode::Shared
|
SharingMode::Shared
|
||||||
})
|
})
|
||||||
.set_usage(Usage::VoiceCommunication)
|
// Usage::Game avoids forcing the Legacy (OpenSL ES) data path
|
||||||
.set_content_type(oboe::ContentType::Speech);
|
// that Usage::VoiceCommunication triggers on most devices.
|
||||||
|
// Android audio routing is already handled by
|
||||||
|
// AudioManager.MODE_IN_COMMUNICATION on the Flutter side.
|
||||||
|
.set_usage(Usage::Game)
|
||||||
|
.set_content_type(oboe::ContentType::Sonification);
|
||||||
|
|
||||||
let render_ref_for_output = render_ref_buf.clone();
|
let render_ref_for_output = render_ref_buf.clone();
|
||||||
|
let event_queue = params.event_producer.queue();
|
||||||
let output_cb = OutputCallback {
|
let output_cb = OutputCallback {
|
||||||
handler: params.handler.clone(),
|
handler: params.handler,
|
||||||
|
event_consumer: AudioEventQueue::consumer(&event_queue),
|
||||||
output_gain: params.output_gain.clone(),
|
output_gain: params.output_gain.clone(),
|
||||||
output_muted: params.output_muted.clone(),
|
output_muted: params.output_muted.clone(),
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
scratch: scratch.clone(),
|
|
||||||
render_reference: render_ref_for_output,
|
render_reference: render_ref_for_output,
|
||||||
audio_processing_stats: audio_processing_stats.clone(),
|
audio_processing_stats: audio_processing_stats.clone(),
|
||||||
pending_render_ref: [0.0_f32; crate::frame::FRAME_10MS_SAMPLES],
|
pending_render_ref: [0.0_f32; crate::frame::FRAME_10MS_SAMPLES],
|
||||||
@@ -806,20 +824,41 @@ impl AndroidVoiceUnit {
|
|||||||
Self::open_output_fallback(
|
Self::open_output_fallback(
|
||||||
cfg,
|
cfg,
|
||||||
&event_tx,
|
&event_tx,
|
||||||
params.handler.clone(),
|
AudioHandler::new(),
|
||||||
|
AudioEventQueue::consumer(&event_queue),
|
||||||
params.output_gain.clone(),
|
params.output_gain.clone(),
|
||||||
params.output_muted.clone(),
|
params.output_muted.clone(),
|
||||||
audio_processing_stats.clone(),
|
audio_processing_stats.clone(),
|
||||||
scratch.clone(),
|
|
||||||
render_ref_buf,
|
render_ref_buf,
|
||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let output_frames_per_burst = output_stream.get_frames_per_burst();
|
||||||
|
if output_frames_per_burst > 0 {
|
||||||
|
let desired = output_frames_per_burst * 2;
|
||||||
|
match output_stream.set_buffer_size_in_frames(desired) {
|
||||||
|
Ok(actual) => {
|
||||||
|
debug!(
|
||||||
|
target: "chanora_audio",
|
||||||
|
desired,
|
||||||
|
actual,
|
||||||
|
"android: output buffer size tuned"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
target: "chanora_audio",
|
||||||
|
error = ?e,
|
||||||
|
"android: output buffer size tuning failed; using device default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let output_perf = perf_from_oboe(output_stream.get_performance_mode());
|
let output_perf = perf_from_oboe(output_stream.get_performance_mode());
|
||||||
let output_share = share_from_oboe(output_stream.get_sharing_mode());
|
let output_share = share_from_oboe(output_stream.get_sharing_mode());
|
||||||
let output_sample_rate = output_stream.get_sample_rate();
|
let output_sample_rate = output_stream.get_sample_rate();
|
||||||
let output_frames_per_burst = output_stream.get_frames_per_burst();
|
|
||||||
|
|
||||||
// SDD-112 / SRS-210: structured "stream opened" event with
|
// SDD-112 / SRS-210: structured "stream opened" event with
|
||||||
// achieved values. No PII; only platform-reported scalars.
|
// achieved values. No PII; only platform-reported scalars.
|
||||||
@@ -1038,19 +1077,19 @@ impl AndroidVoiceUnit {
|
|||||||
fn open_output_fallback(
|
fn open_output_fallback(
|
||||||
cfg: &AndroidVoiceStreamConfig,
|
cfg: &AndroidVoiceStreamConfig,
|
||||||
event_tx: &BackendEventTx,
|
event_tx: &BackendEventTx,
|
||||||
handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
handler: AudioHandler<SessionAudioId>,
|
||||||
|
event_consumer: crate::audio_event_queue::AudioEventConsumer,
|
||||||
output_gain: Arc<AtomicU32>,
|
output_gain: Arc<AtomicU32>,
|
||||||
output_muted: Arc<AtomicBool>,
|
output_muted: Arc<AtomicBool>,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||||
scratch: Arc<Mutex<Vec<f32>>>,
|
|
||||||
render_reference: Arc<RenderReferenceBuffer>,
|
render_reference: Arc<RenderReferenceBuffer>,
|
||||||
) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> {
|
) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> {
|
||||||
let cb = OutputCallback {
|
let cb = OutputCallback {
|
||||||
handler,
|
handler,
|
||||||
|
event_consumer,
|
||||||
output_gain,
|
output_gain,
|
||||||
output_muted,
|
output_muted,
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
scratch,
|
|
||||||
render_reference,
|
render_reference,
|
||||||
audio_processing_stats,
|
audio_processing_stats,
|
||||||
pending_render_ref: [0.0_f32; crate::frame::FRAME_10MS_SAMPLES],
|
pending_render_ref: [0.0_f32; crate::frame::FRAME_10MS_SAMPLES],
|
||||||
@@ -1059,12 +1098,13 @@ impl AndroidVoiceUnit {
|
|||||||
let builder = AudioStreamBuilder::default()
|
let builder = AudioStreamBuilder::default()
|
||||||
.set_direction::<OboeOutput>()
|
.set_direction::<OboeOutput>()
|
||||||
.set_sample_rate(cfg.sample_rate as i32)
|
.set_sample_rate(cfg.sample_rate as i32)
|
||||||
.set_channel_count::<Mono>()
|
.set_channel_count::<Stereo>()
|
||||||
.set_format::<i16>()
|
.set_format::<f32>()
|
||||||
.set_performance_mode(PerformanceMode::LowLatency)
|
.set_performance_mode(PerformanceMode::LowLatency)
|
||||||
.set_sharing_mode(SharingMode::Shared)
|
.set_sharing_mode(SharingMode::Shared)
|
||||||
.set_usage(Usage::VoiceCommunication)
|
// Same Usage::Game rationale as primary output builder above.
|
||||||
.set_content_type(oboe::ContentType::Speech)
|
.set_usage(Usage::Game)
|
||||||
|
.set_content_type(oboe::ContentType::Sonification)
|
||||||
.set_callback(cb);
|
.set_callback(cb);
|
||||||
builder
|
builder
|
||||||
.open_stream()
|
.open_stream()
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chanora_protocol::InAudioBuf;
|
||||||
|
use crossbeam::queue::ArrayQueue;
|
||||||
|
|
||||||
|
use crate::engine::SessionAudioId;
|
||||||
|
|
||||||
|
const PACKET_QUEUE_CAPACITY: usize = 100;
|
||||||
|
const CONTROL_QUEUE_CAPACITY: usize = 32;
|
||||||
|
|
||||||
|
/// A raw inbound voice packet waiting to be inserted into AudioHandler.
|
||||||
|
pub struct AudioPacket {
|
||||||
|
/// Client whose TeamSpeak audio packet this belongs to.
|
||||||
|
pub client_id: SessionAudioId,
|
||||||
|
/// Raw inbound TeamSpeak audio payload accepted by AudioHandler::handle_packet.
|
||||||
|
pub data: InAudioBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Control commands from the main thread to the audio callback.
|
||||||
|
pub enum AudioCommand {
|
||||||
|
/// Set a client's output volume.
|
||||||
|
SetVolume(SessionAudioId, f32),
|
||||||
|
/// Remove a client's decode queue.
|
||||||
|
// TODO: Wire to client disconnect path; handled in callback but no
|
||||||
|
// producer currently pushes this command.
|
||||||
|
RemoveClient(SessionAudioId),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lock-free bridge between the inbound forwarder / main thread and the
|
||||||
|
/// audio callback. The callback owns the consumer halves.
|
||||||
|
pub struct AudioEventQueue {
|
||||||
|
/// Bounded lossy queue for raw voice packets. On overflow, the push
|
||||||
|
/// fails and the packet is dropped (counted via `packets_dropped`).
|
||||||
|
/// Capacity: 100 packets (~2 seconds at 50pps, far more than needed).
|
||||||
|
pub packet_queue: ArrayQueue<AudioPacket>,
|
||||||
|
/// Bounded reliable queue for control commands (volume, client removal).
|
||||||
|
/// On overflow, the caller retries. Capacity: 32 commands.
|
||||||
|
pub control_queue: ArrayQueue<AudioCommand>,
|
||||||
|
/// Atomic counter for dropped packets (for diagnostics).
|
||||||
|
pub packets_dropped: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioEventQueue {
|
||||||
|
/// Create the Android audio event bridge with fixed queue capacities.
|
||||||
|
pub fn new() -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
packet_queue: ArrayQueue::new(PACKET_QUEUE_CAPACITY),
|
||||||
|
control_queue: ArrayQueue::new(CONTROL_QUEUE_CAPACITY),
|
||||||
|
packets_dropped: AtomicU64::new(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a producer handle sharing this queue.
|
||||||
|
pub fn producer(queue: &Arc<Self>) -> AudioEventProducer {
|
||||||
|
AudioEventProducer {
|
||||||
|
queue: Arc::clone(queue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a consumer handle sharing this queue.
|
||||||
|
pub fn consumer(queue: &Arc<Self>) -> AudioEventConsumer {
|
||||||
|
AudioEventConsumer {
|
||||||
|
queue: Arc::clone(queue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Producer side used by the inbound forwarder and engine control methods.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AudioEventProducer {
|
||||||
|
queue: Arc<AudioEventQueue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioEventProducer {
|
||||||
|
/// Push a raw voice packet, incrementing the drop counter if full.
|
||||||
|
pub fn push_packet(&self, packet: AudioPacket) -> Result<(), AudioPacket> {
|
||||||
|
self.queue.packet_queue.push(packet).map_err(|packet| {
|
||||||
|
self.queue.packets_dropped.fetch_add(1, Ordering::Relaxed);
|
||||||
|
packet
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a control command, returning it unchanged if the queue is full.
|
||||||
|
pub fn push_control(&self, cmd: AudioCommand) -> Result<(), AudioCommand> {
|
||||||
|
self.queue.control_queue.push(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared queue backing this producer.
|
||||||
|
pub fn queue(&self) -> Arc<AudioEventQueue> {
|
||||||
|
Arc::clone(&self.queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumer side used by the Android output callback.
|
||||||
|
pub struct AudioEventConsumer {
|
||||||
|
queue: Arc<AudioEventQueue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioEventConsumer {
|
||||||
|
/// Pop up to `cap` queued packets.
|
||||||
|
pub fn drain_packets(&self, cap: usize) -> impl Iterator<Item = AudioPacket> + '_ {
|
||||||
|
let mut drained = 0;
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
if drained >= cap {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let packet = self.queue.packet_queue.pop();
|
||||||
|
if packet.is_some() {
|
||||||
|
drained += 1;
|
||||||
|
}
|
||||||
|
packet
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop all currently queued controls.
|
||||||
|
pub fn drain_controls(&self) -> impl Iterator<Item = AudioCommand> + '_ {
|
||||||
|
std::iter::from_fn(move || self.queue.control_queue.pop())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,8 @@ use tsclientlib::audio::AudioHandler;
|
|||||||
|
|
||||||
use chanora_protocol::{InboundVoice, OutPacket};
|
use chanora_protocol::{InboundVoice, OutPacket};
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use crate::audio_event_queue::{AudioCommand, AudioEventQueue, AudioPacket};
|
||||||
use crate::AudioError;
|
use crate::AudioError;
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
@@ -305,8 +307,11 @@ pub struct AudioEngine {
|
|||||||
output_muted: Arc<AtomicBool>,
|
output_muted: Arc<AtomicBool>,
|
||||||
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
|
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
audio_event_producer: crate::audio_event_queue::AudioEventProducer,
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
||||||
@@ -482,7 +487,7 @@ impl AudioEngine {
|
|||||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||||
transmit_gate: crate::ptt::AudioTransmitGate,
|
transmit_gate: crate::ptt::AudioTransmitGate,
|
||||||
frames_sent: Arc<AtomicU32>,
|
frames_sent: Arc<AtomicU32>,
|
||||||
audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
event_producer: crate::audio_event_queue::AudioEventProducer,
|
||||||
output_gain: Arc<AtomicU32>,
|
output_gain: Arc<AtomicU32>,
|
||||||
output_muted: Arc<AtomicBool>,
|
output_muted: Arc<AtomicBool>,
|
||||||
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
|
||||||
@@ -552,7 +557,8 @@ impl AudioEngine {
|
|||||||
transmit_active: transmit_gate.flag_arc(),
|
transmit_active: transmit_gate.flag_arc(),
|
||||||
frames_sent: frames_sent.clone(),
|
frames_sent: frames_sent.clone(),
|
||||||
mic_gain,
|
mic_gain,
|
||||||
handler: audio_handler.clone(),
|
handler: AudioHandler::new(),
|
||||||
|
event_producer: event_producer.clone(),
|
||||||
output_gain: output_gain.clone(),
|
output_gain: output_gain.clone(),
|
||||||
output_muted: output_muted.clone(),
|
output_muted: output_muted.clone(),
|
||||||
voice_activity_selector: voice_activity_selector.clone(),
|
voice_activity_selector: voice_activity_selector.clone(),
|
||||||
@@ -572,7 +578,7 @@ impl AudioEngine {
|
|||||||
voice_out_tx.clone(),
|
voice_out_tx.clone(),
|
||||||
transmit_gate.clone(),
|
transmit_gate.clone(),
|
||||||
frames_sent.clone(),
|
frames_sent.clone(),
|
||||||
audio_handler.clone(),
|
event_producer.clone(),
|
||||||
output_gain.clone(),
|
output_gain.clone(),
|
||||||
output_muted.clone(),
|
output_muted.clone(),
|
||||||
voice_activity_selector.clone(),
|
voice_activity_selector.clone(),
|
||||||
@@ -1007,8 +1013,8 @@ impl AudioEngine {
|
|||||||
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
|
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
|
||||||
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
|
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
|
||||||
|
|
||||||
let audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>> =
|
let event_queue = AudioEventQueue::new();
|
||||||
Arc::new(Mutex::new(AudioHandler::new()));
|
let event_producer = AudioEventQueue::producer(&event_queue);
|
||||||
|
|
||||||
if !cfg.mobile_voice_preset {
|
if !cfg.mobile_voice_preset {
|
||||||
return Err(AudioError::Backend(
|
return Err(AudioError::Backend(
|
||||||
@@ -1069,7 +1075,8 @@ impl AudioEngine {
|
|||||||
transmit_active: transmit_flag_for_capture,
|
transmit_active: transmit_flag_for_capture,
|
||||||
frames_sent: frames_sent.clone(),
|
frames_sent: frames_sent.clone(),
|
||||||
mic_gain: cfg.mic_gain,
|
mic_gain: cfg.mic_gain,
|
||||||
handler: audio_handler.clone(),
|
handler: AudioHandler::new(),
|
||||||
|
event_producer: event_producer.clone(),
|
||||||
output_gain: output_gain.clone(),
|
output_gain: output_gain.clone(),
|
||||||
output_muted: output_muted.clone(),
|
output_muted: output_muted.clone(),
|
||||||
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
||||||
@@ -1103,7 +1110,7 @@ impl AudioEngine {
|
|||||||
voice_out_tx.clone(),
|
voice_out_tx.clone(),
|
||||||
transmit_gate.clone(),
|
transmit_gate.clone(),
|
||||||
frames_sent.clone(),
|
frames_sent.clone(),
|
||||||
audio_handler.clone(),
|
event_producer.clone(),
|
||||||
output_gain.clone(),
|
output_gain.clone(),
|
||||||
output_muted.clone(),
|
output_muted.clone(),
|
||||||
cfg.voice_activity_selector.clone(),
|
cfg.voice_activity_selector.clone(),
|
||||||
@@ -1151,7 +1158,7 @@ impl AudioEngine {
|
|||||||
let capture_active = true;
|
let capture_active = true;
|
||||||
|
|
||||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||||
let handler_for_task = audio_handler.clone();
|
let event_producer_for_task = event_producer.clone();
|
||||||
let frames_received_for_task = frames_received.clone();
|
let frames_received_for_task = frames_received.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
@@ -1164,10 +1171,8 @@ impl AudioEngine {
|
|||||||
match item {
|
match item {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
let id = SessionAudioId(v.from_client);
|
let id = SessionAudioId(v.from_client);
|
||||||
let mut h = handler_for_task.lock().unwrap();
|
let packet = AudioPacket { client_id: id, data: v.packet };
|
||||||
if let Err(e) = h.handle_packet(id, v.packet) {
|
if event_producer_for_task.push_packet(packet).is_ok() {
|
||||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
|
||||||
} else {
|
|
||||||
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
frames_received_for_task.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1186,7 +1191,7 @@ impl AudioEngine {
|
|||||||
output_muted,
|
output_muted,
|
||||||
audio_processing_config,
|
audio_processing_config,
|
||||||
audio_processing_stats,
|
audio_processing_stats,
|
||||||
audio_handler,
|
audio_event_producer: event_producer,
|
||||||
voice_out_tx,
|
voice_out_tx,
|
||||||
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
||||||
mic_gain: cfg.mic_gain,
|
mic_gain: cfg.mic_gain,
|
||||||
@@ -1467,7 +1472,8 @@ impl AudioEngine {
|
|||||||
transmit_active: self.transmit_gate.flag_arc(),
|
transmit_active: self.transmit_gate.flag_arc(),
|
||||||
frames_sent: self.frames_sent.clone(),
|
frames_sent: self.frames_sent.clone(),
|
||||||
mic_gain: self.mic_gain,
|
mic_gain: self.mic_gain,
|
||||||
handler: self.audio_handler.clone(),
|
handler: AudioHandler::new(),
|
||||||
|
event_producer: self.audio_event_producer.clone(),
|
||||||
output_gain: self.output_gain.clone(),
|
output_gain: self.output_gain.clone(),
|
||||||
output_muted: self.output_muted.clone(),
|
output_muted: self.output_muted.clone(),
|
||||||
voice_activity_selector: self.voice_activity_selector.clone(),
|
voice_activity_selector: self.voice_activity_selector.clone(),
|
||||||
@@ -1488,7 +1494,7 @@ impl AudioEngine {
|
|||||||
self.voice_out_tx.clone(),
|
self.voice_out_tx.clone(),
|
||||||
self.transmit_gate.clone(),
|
self.transmit_gate.clone(),
|
||||||
self.frames_sent.clone(),
|
self.frames_sent.clone(),
|
||||||
self.audio_handler.clone(),
|
self.audio_event_producer.clone(),
|
||||||
self.output_gain.clone(),
|
self.output_gain.clone(),
|
||||||
self.output_muted.clone(),
|
self.output_muted.clone(),
|
||||||
self.voice_activity_selector.clone(),
|
self.voice_activity_selector.clone(),
|
||||||
@@ -1655,20 +1661,42 @@ impl AudioEngine {
|
|||||||
/// `0.0..4.0`.
|
/// `0.0..4.0`.
|
||||||
pub fn set_client_volume(&self, client_id: u64, volume: f32) {
|
pub fn set_client_volume(&self, client_id: u64, volume: f32) {
|
||||||
let clamped = volume.clamp(0.0, 4.0);
|
let clamped = volume.clamp(0.0, 4.0);
|
||||||
match self.audio_handler.lock() {
|
#[cfg(target_os = "android")]
|
||||||
Ok(mut h) => {
|
{
|
||||||
if let Some(q) = h.get_mut_queues().get_mut(&SessionAudioId(client_id)) {
|
let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped);
|
||||||
q.volume = clamped;
|
for _ in 0..64 {
|
||||||
|
match self.audio_event_producer.push_control(cmd) {
|
||||||
|
Ok(()) => return,
|
||||||
|
Err(returned) => {
|
||||||
|
cmd = returned;
|
||||||
|
std::thread::yield_now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
tracing::warn!(
|
||||||
tracing::warn!(
|
target: "chanora_audio",
|
||||||
target: "chanora_audio",
|
client_id,
|
||||||
client_id,
|
volume = clamped,
|
||||||
volume = clamped,
|
"set_client_volume: control queue full after 64 retries — volume not applied"
|
||||||
error = %e,
|
);
|
||||||
"set_client_volume: audio_handler lock poisoned — volume not applied"
|
}
|
||||||
);
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
match self.audio_handler.lock() {
|
||||||
|
Ok(mut h) => {
|
||||||
|
if let Some(q) = h.get_mut_queues().get_mut(&SessionAudioId(client_id)) {
|
||||||
|
q.volume = clamped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "chanora_audio",
|
||||||
|
client_id,
|
||||||
|
volume = clamped,
|
||||||
|
error = %e,
|
||||||
|
"set_client_volume: audio_handler lock poisoned — volume not applied"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
mod audio_event_queue;
|
||||||
pub mod audio_processing;
|
pub mod audio_processing;
|
||||||
pub mod debug_wav;
|
pub mod debug_wav;
|
||||||
mod engine;
|
mod engine;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ pub type BackendEventTx = mpsc::UnboundedSender<BackendEvent>;
|
|||||||
pub type AudioSessionId = i32;
|
pub type AudioSessionId = i32;
|
||||||
|
|
||||||
/// Engine-owned state shared with mobile voice audio callbacks.
|
/// Engine-owned state shared with mobile voice audio callbacks.
|
||||||
#[derive(Clone)]
|
#[cfg_attr(not(target_os = "android"), derive(Clone))]
|
||||||
pub(crate) struct VoiceAudioParams {
|
pub(crate) struct VoiceAudioParams {
|
||||||
/// Opus-encoded voice packets sent on this channel toward the
|
/// Opus-encoded voice packets sent on this channel toward the
|
||||||
/// protocol layer.
|
/// protocol layer.
|
||||||
@@ -78,8 +78,15 @@ pub(crate) struct VoiceAudioParams {
|
|||||||
pub frames_sent: Arc<AtomicU32>,
|
pub frames_sent: Arc<AtomicU32>,
|
||||||
/// Pre-encode amplitude scale (1.0 = unity).
|
/// Pre-encode amplitude scale (1.0 = unity).
|
||||||
pub mic_gain: f32,
|
pub mic_gain: f32,
|
||||||
|
/// AudioHandler owned by the Android output callback.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub handler: AudioHandler<SessionAudioId>,
|
||||||
|
/// 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
|
/// AudioHandler that inbound decode+mix feeds into; the output
|
||||||
/// callback pulls mixed stereo f32 from it.
|
/// callback pulls mixed stereo f32 from it.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
pub handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
|
||||||
/// Master output gain (f32 bits stored in AtomicU32 for lock-free
|
/// Master output gain (f32 bits stored in AtomicU32 for lock-free
|
||||||
/// cross-thread read from the realtime audio callback).
|
/// cross-thread read from the realtime audio callback).
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ those terms.
|
|||||||
|
|
||||||
| License | Crate count |
|
| License | Crate count |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `Apache License 2.0` | 331 |
|
| `Apache License 2.0` | 333 |
|
||||||
| `MIT License` | 74 |
|
| `MIT License` | 74 |
|
||||||
| `Unicode License v3` | 19 |
|
| `Unicode License v3` | 19 |
|
||||||
| `BSD 3-Clause "New" or "Revised" License` | 14 |
|
| `BSD 3-Clause "New" or "Revised" License` | 14 |
|
||||||
@@ -117,6 +117,7 @@ those terms.
|
|||||||
| pin-utils | 0.1.0 | `Apache License 2.0` | <https://github.com/rust-lang-nursery/pin-utils> |
|
| pin-utils | 0.1.0 | `Apache License 2.0` | <https://github.com/rust-lang-nursery/pin-utils> |
|
||||||
| ecdsa | 0.16.9 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/ecdsa> |
|
| ecdsa | 0.16.9 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/ecdsa> |
|
||||||
| rfc6979 | 0.4.0 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/rfc6979> |
|
| rfc6979 | 0.4.0 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/rfc6979> |
|
||||||
|
| crossbeam | 0.8.4 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| ppv-lite86 | 0.2.21 | `Apache License 2.0` | <https://github.com/cryptocorrosion/cryptocorrosion> |
|
| ppv-lite86 | 0.2.21 | `Apache License 2.0` | <https://github.com/cryptocorrosion/cryptocorrosion> |
|
||||||
| rustls-pki-types | 1.14.1 | `Apache License 2.0` | <https://github.com/rustls/pki-types> |
|
| rustls-pki-types | 1.14.1 | `Apache License 2.0` | <https://github.com/rustls/pki-types> |
|
||||||
| keyring | 3.6.3 | `Apache License 2.0` | <https://github.com/hwchen/keyring-rs.git> |
|
| keyring | 3.6.3 | `Apache License 2.0` | <https://github.com/hwchen/keyring-rs.git> |
|
||||||
@@ -144,6 +145,7 @@ those terms.
|
|||||||
| critical-section | 1.2.0 | `Apache License 2.0` | <https://github.com/rust-embedded/critical-section> |
|
| critical-section | 1.2.0 | `Apache License 2.0` | <https://github.com/rust-embedded/critical-section> |
|
||||||
| crossbeam-channel | 0.5.15 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
| crossbeam-channel | 0.5.15 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| crossbeam-epoch | 0.9.18 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
| crossbeam-epoch | 0.9.18 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
|
| crossbeam-queue | 0.3.12 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| crossbeam-utils | 0.8.21 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
| crossbeam-utils | 0.8.21 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| dbus-secret-service | 4.1.0 | `Apache License 2.0` | <https://github.com/brotskydotcom/dbus-secret-service.git> |
|
| dbus-secret-service | 4.1.0 | `Apache License 2.0` | <https://github.com/brotskydotcom/dbus-secret-service.git> |
|
||||||
| displaydoc | 0.2.6 | `Apache License 2.0` | <https://github.com/yaahc/displaydoc> |
|
| displaydoc | 0.2.6 | `Apache License 2.0` | <https://github.com/yaahc/displaydoc> |
|
||||||
@@ -4993,6 +4995,213 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2019 The Crossbeam Project Developers
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apache License 2.0
|
||||||
|
|
||||||
|
```
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
To apply the Apache License to your work, attach the following
|
||||||
|
|||||||
Reference in New Issue
Block a user