refactor: reuse built-ins and shared helpers
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use crate::ptt::{AudioTransmitGate, PttBackendDescriptor};
|
use crate::ptt::{AudioTransmitGate, PttBackendDescriptor};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
mod focused;
|
mod focused;
|
||||||
|
|
||||||
@@ -108,34 +109,50 @@ impl fmt::Display for PttInputClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Errors raised by a desktop PTT backend.
|
/// Errors raised by a desktop PTT backend.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PttBackendError {
|
pub enum PttBackendError {
|
||||||
/// The OS rejected the backend initialisation (e.g. Raw Input
|
/// The OS rejected the backend initialisation (e.g. Raw Input
|
||||||
/// registration failed, event tap creation failed).
|
/// registration failed, event tap creation failed).
|
||||||
|
#[error("init failed: {0}")]
|
||||||
Init(String),
|
Init(String),
|
||||||
/// The user-granted permission required for global capture is
|
/// The user-granted permission required for global capture is
|
||||||
/// not granted (typically macOS Input Monitoring / Accessibility).
|
/// not granted (typically macOS Input Monitoring / Accessibility).
|
||||||
|
#[error("permission denied")]
|
||||||
PermissionDenied,
|
PermissionDenied,
|
||||||
/// The display server or compositor does not expose the
|
/// The display server or compositor does not expose the
|
||||||
/// expected interface (typically a non-tested Linux compositor).
|
/// expected interface (typically a non-tested Linux compositor).
|
||||||
|
#[error("unsupported environment")]
|
||||||
UnsupportedEnvironment,
|
UnsupportedEnvironment,
|
||||||
/// Caller submitted a binding whose `platform_key` cannot be
|
/// Caller submitted a binding whose `platform_key` cannot be
|
||||||
/// parsed in the active OS.
|
/// parsed in the active OS.
|
||||||
|
#[error("invalid binding: {0}")]
|
||||||
InvalidBinding(String),
|
InvalidBinding(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PttBackendError {
|
#[cfg(test)]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
mod tests {
|
||||||
match self {
|
use super::*;
|
||||||
Self::Init(s) => write!(f, "init failed: {s}"),
|
|
||||||
Self::PermissionDenied => f.write_str("permission denied"),
|
|
||||||
Self::UnsupportedEnvironment => f.write_str("unsupported environment"),
|
|
||||||
Self::InvalidBinding(s) => write!(f, "invalid binding: {s}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for PttBackendError {}
|
#[test]
|
||||||
|
fn ptt_backend_error_display_strings_stay_stable() {
|
||||||
|
assert_eq!(
|
||||||
|
PttBackendError::Init("rawinput".into()).to_string(),
|
||||||
|
"init failed: rawinput"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PttBackendError::PermissionDenied.to_string(),
|
||||||
|
"permission denied"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PttBackendError::UnsupportedEnvironment.to_string(),
|
||||||
|
"unsupported environment"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PttBackendError::InvalidBinding("bad key".into()).to_string(),
|
||||||
|
"invalid binding: bad key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cross-platform desktop PTT backend (SDD-081).
|
/// Cross-platform desktop PTT backend (SDD-081).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/// Diagnostics returned by render downmix helpers.
|
/// Diagnostics returned by render downmix helpers.
|
||||||
|
#[cfg(any(target_os = "ios", test))]
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
pub(crate) struct RenderDownmixStats {
|
pub(crate) struct RenderDownmixStats {
|
||||||
/// Peak absolute sample magnitude after i16 conversion.
|
/// Peak absolute sample magnitude after i16 conversion.
|
||||||
@@ -7,47 +8,7 @@ pub(crate) struct RenderDownmixStats {
|
|||||||
pub clipped_samples: u64,
|
pub clipped_samples: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downmix interleaved stereo f32 samples into mono i16 samples.
|
|
||||||
///
|
|
||||||
/// The helper is allocation-free and safe for realtime render callbacks.
|
|
||||||
/// If the stereo source is shorter than expected, the remainder of `out`
|
|
||||||
/// is filled with silence.
|
|
||||||
#[cfg(any(target_os = "ios", test))]
|
#[cfg(any(target_os = "ios", test))]
|
||||||
pub(crate) fn downmix_stereo_f32_to_mono_i16(
|
|
||||||
stereo: &[f32],
|
|
||||||
out: &mut [i16],
|
|
||||||
gain: f32,
|
|
||||||
muted: bool,
|
|
||||||
) -> RenderDownmixStats {
|
|
||||||
if muted {
|
|
||||||
out.fill(0);
|
|
||||||
return RenderDownmixStats::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
let available_frames = stereo.len() / 2;
|
|
||||||
if available_frames < out.len() {
|
|
||||||
out.fill(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut peak = 0_u16;
|
|
||||||
let mut clipped_samples = 0_u64;
|
|
||||||
for (dst, lr) in out.iter_mut().zip(stereo.chunks_exact(2)) {
|
|
||||||
let mono = (lr[0] + lr[1]) * 0.5 * gain;
|
|
||||||
let clamped = mono.clamp(-1.0, 1.0);
|
|
||||||
if (mono - clamped).abs() > f32::EPSILON {
|
|
||||||
clipped_samples = clipped_samples.saturating_add(1);
|
|
||||||
}
|
|
||||||
let sample = (clamped * i16::MAX as f32) as i16;
|
|
||||||
*dst = sample;
|
|
||||||
peak = peak.max(sample.unsigned_abs());
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderDownmixStats {
|
|
||||||
peak_i16: peak.min(i16::MAX as u16) as i16,
|
|
||||||
clipped_samples,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn downmix_stereo_f32_to_interleaved_i16(
|
pub(crate) fn downmix_stereo_f32_to_interleaved_i16(
|
||||||
stereo: &[f32],
|
stereo: &[f32],
|
||||||
out: &mut [i16],
|
out: &mut [i16],
|
||||||
@@ -122,10 +83,7 @@ pub(crate) fn limit_peak_inplace(samples: &mut [f32], threshold: f32) -> f32 {
|
|||||||
if threshold <= 0.0 || !threshold.is_finite() {
|
if threshold <= 0.0 || !threshold.is_finite() {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
let peak = samples
|
let peak = samples.iter().map(|s| s.abs()).fold(0.0_f32, f32::max);
|
||||||
.iter()
|
|
||||||
.map(|s| s.abs())
|
|
||||||
.fold(0.0_f32, f32::max);
|
|
||||||
if peak <= threshold {
|
if peak <= threshold {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
@@ -145,7 +103,7 @@ mod tests {
|
|||||||
let stereo = [1.0_f32, 1.0, 0.25, -0.25, -2.0, -2.0];
|
let stereo = [1.0_f32, 1.0, 0.25, -0.25, -2.0, -2.0];
|
||||||
let mut out = [0_i16; 3];
|
let mut out = [0_i16; 3];
|
||||||
|
|
||||||
let stats = downmix_stereo_f32_to_mono_i16(&stereo, &mut out, 2.0, false);
|
let stats = downmix_stereo_f32_to_interleaved_i16(&stereo, &mut out, 1, 2.0, false);
|
||||||
|
|
||||||
assert_eq!(out[0], i16::MAX);
|
assert_eq!(out[0], i16::MAX);
|
||||||
assert_eq!(out[1], 0);
|
assert_eq!(out[1], 0);
|
||||||
@@ -159,7 +117,7 @@ mod tests {
|
|||||||
let stereo = [1.0_f32, 1.0, -1.0, -1.0];
|
let stereo = [1.0_f32, 1.0, -1.0, -1.0];
|
||||||
let mut out = [123_i16; 2];
|
let mut out = [123_i16; 2];
|
||||||
|
|
||||||
let stats = downmix_stereo_f32_to_mono_i16(&stereo, &mut out, 1.0, true);
|
let stats = downmix_stereo_f32_to_interleaved_i16(&stereo, &mut out, 1, 1.0, true);
|
||||||
|
|
||||||
assert_eq!(out, [0, 0]);
|
assert_eq!(out, [0, 0]);
|
||||||
assert_eq!(stats, RenderDownmixStats::default());
|
assert_eq!(stats, RenderDownmixStats::default());
|
||||||
@@ -234,7 +192,7 @@ mod tests {
|
|||||||
let mut scratch = [1.0_f32, 1.0, -0.5, -0.5, 0.8, 0.8];
|
let mut scratch = [1.0_f32, 1.0, -0.5, -0.5, 0.8, 0.8];
|
||||||
limit_peak_inplace(&mut scratch, 0.95);
|
limit_peak_inplace(&mut scratch, 0.95);
|
||||||
let mut out = [0_i16; 3];
|
let mut out = [0_i16; 3];
|
||||||
let stats = downmix_stereo_f32_to_mono_i16(&scratch, &mut out, 1.0, false);
|
let stats = downmix_stereo_f32_to_interleaved_i16(&scratch, &mut out, 1, 1.0, false);
|
||||||
assert_eq!(stats.clipped_samples, 0);
|
assert_eq!(stats.clipped_samples, 0);
|
||||||
assert!(stats.peak_i16 < i16::MAX);
|
assert!(stats.peak_i16 < i16::MAX);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashSet, VecDeque};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -712,7 +712,7 @@ impl DiagnosticExport {
|
|||||||
/// diagnostic export and state-sync replay verification.
|
/// diagnostic export and state-sync replay verification.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProtocolEventRecorder {
|
pub struct ProtocolEventRecorder {
|
||||||
events: Vec<String>,
|
events: VecDeque<String>,
|
||||||
capacity: usize,
|
capacity: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,17 +720,20 @@ impl ProtocolEventRecorder {
|
|||||||
/// Create a recorder with the given ring-buffer capacity.
|
/// Create a recorder with the given ring-buffer capacity.
|
||||||
pub fn new(capacity: usize) -> Self {
|
pub fn new(capacity: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
events: Vec::with_capacity(capacity),
|
events: VecDeque::with_capacity(capacity),
|
||||||
capacity,
|
capacity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, ts: &str, kind: &str, detail: &str) {
|
fn push(&mut self, ts: &str, kind: &str, detail: &str) {
|
||||||
|
if self.capacity == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let s = format!("[{ts}] {kind}: {detail}");
|
let s = format!("[{ts}] {kind}: {detail}");
|
||||||
if self.events.len() >= self.capacity {
|
if self.events.len() >= self.capacity {
|
||||||
self.events.remove(0);
|
self.events.pop_front();
|
||||||
}
|
}
|
||||||
self.events.push(s);
|
self.events.push_back(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record a successful connection.
|
/// Record a successful connection.
|
||||||
@@ -777,12 +780,12 @@ impl ProtocolEventRecorder {
|
|||||||
|
|
||||||
/// Drain all recorded events and reset the buffer.
|
/// Drain all recorded events and reset the buffer.
|
||||||
pub fn drain(&mut self) -> Vec<String> {
|
pub fn drain(&mut self) -> Vec<String> {
|
||||||
std::mem::take(&mut self.events)
|
self.events.drain(..).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot all recorded events without clearing the buffer.
|
/// Snapshot all recorded events without clearing the buffer.
|
||||||
pub fn snapshot(&self) -> Vec<String> {
|
pub fn snapshot(&self) -> Vec<String> {
|
||||||
self.events.clone()
|
self.events.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,4 +1106,13 @@ mod tests {
|
|||||||
assert_eq!(first, second);
|
assert_eq!(first, second);
|
||||||
assert_eq!(drained, first);
|
assert_eq!(drained, first);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocol_event_zero_capacity_drops_events() {
|
||||||
|
let mut recorder = ProtocolEventRecorder::new(0);
|
||||||
|
recorder.record_connected("Server");
|
||||||
|
|
||||||
|
assert!(recorder.snapshot().is_empty());
|
||||||
|
assert!(recorder.drain().is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user