mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
chore(repo): remove dead code and useless tests (#3375)
This commit is contained in:
1 parent
7e177a1c90
commit
1cd62eb661
1391 files changed
+783
-120415
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -4,50 +4,12 @@
|
||||
|
||||
use crate::routing::{PropMap, RoutingRule, SelfIdentity};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RoutingGraphSnapshot {
|
||||
pub backend: String,
|
||||
pub nodes: Vec<RoutingGraphNode>,
|
||||
pub ports: Vec<RoutingGraphPort>,
|
||||
pub owned_links: Vec<RoutingGraphLink>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RoutingGraphNode {
|
||||
pub id: u32,
|
||||
pub props: PropMap,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RoutingGraphPort {
|
||||
pub id: u32,
|
||||
pub node_id: u32,
|
||||
pub direction: String,
|
||||
pub channel: String,
|
||||
pub props: PropMap,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct RoutingGraphLink {
|
||||
pub output_node_id: u32,
|
||||
pub output_port_id: u32,
|
||||
pub input_node_id: u32,
|
||||
pub input_port_id: u32,
|
||||
}
|
||||
|
||||
pub trait CaptureBridge: Send + Sync {
|
||||
fn inventory(&self) -> Vec<PropMap>;
|
||||
fn apply(&self, rule: RoutingRule) -> bool;
|
||||
fn release(&self);
|
||||
fn populate_self_identity(&self, identity: SelfIdentity);
|
||||
fn backend_name(&self) -> &'static str;
|
||||
|
||||
fn routing_graph(&self) -> RoutingGraphSnapshot {
|
||||
RoutingGraphSnapshot {
|
||||
backend: self.backend_name().to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DirectCapture: Send + Sync {
|
||||
@@ -65,14 +27,6 @@ pub trait DirectCapture: Send + Sync {
|
||||
}
|
||||
|
||||
fn clear_screen_audio_sink(&self) {}
|
||||
|
||||
fn routing_graph(&self) -> RoutingGraphSnapshot {
|
||||
RoutingGraphSnapshot::default()
|
||||
}
|
||||
|
||||
fn last_push_ns_arc(&self) -> Option<std::sync::Arc<std::sync::atomic::AtomicU64>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CapturedFrame {
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#![allow(dead_code)]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub const AUDIO_BUFFERING_MAX_TICKS: u32 = 64;
|
||||
|
||||
pub const SOURCE_RESET_AFTER_BUFFERED_TICKS: u32 = 128;
|
||||
|
||||
pub const SOURCE_STALE_AFTER_NS: u64 = 5_000_000_000;
|
||||
|
||||
pub const NEVER_PUSHED_SENTINEL: u64 = u64::MAX;
|
||||
|
||||
pub fn compute_source_age_ns(last_push_ns: u64, registered_at_ns: u64, now_ns: u64) -> u64 {
|
||||
let baseline_ns = if last_push_ns == NEVER_PUSHED_SENTINEL {
|
||||
registered_at_ns
|
||||
} else {
|
||||
last_push_ns
|
||||
};
|
||||
assert!(baseline_ns != NEVER_PUSHED_SENTINEL);
|
||||
if now_ns <= baseline_ns {
|
||||
return 0;
|
||||
}
|
||||
now_ns - baseline_ns
|
||||
}
|
||||
|
||||
pub const SAMPLE_RATE_HZ_MIN: u32 = 8_000;
|
||||
pub const SAMPLE_RATE_HZ_MAX: u32 = 384_000;
|
||||
|
||||
pub const TICK_PERIOD_NS_MIN: u64 = 1_000_000;
|
||||
pub const TICK_PERIOD_NS_MAX: u64 = 100_000_000;
|
||||
|
||||
pub const BUFFERED_FRAMES_MAX: u64 = 1 << 28;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IgnoreAudioDecision {
|
||||
Mix,
|
||||
IgnoreThisTick,
|
||||
ResetSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IgnoreAudioResetReason {
|
||||
BufferOverflow,
|
||||
StaleSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IgnoreAudioSourceState {
|
||||
pub id: u64,
|
||||
pub buffered_frames: u64,
|
||||
pub last_frame_age_ns: u64,
|
||||
pub is_muted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IgnoreAudioTick {
|
||||
pub at_ns: u64,
|
||||
pub period_ns: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IgnoreAudioMetrics {
|
||||
pub ignored_tick_count: u64,
|
||||
pub reset_count: u64,
|
||||
}
|
||||
|
||||
impl IgnoreAudioMetrics {
|
||||
pub const ZERO: IgnoreAudioMetrics = IgnoreAudioMetrics {
|
||||
ignored_tick_count: 0,
|
||||
reset_count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IgnoreAudioSourceResetEvent {
|
||||
pub source_id: u64,
|
||||
pub at_ns: u64,
|
||||
pub buffered_frames_at_reset: u64,
|
||||
pub last_frame_age_ns: u64,
|
||||
pub reason: IgnoreAudioResetReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IgnoreAudioEvaluation {
|
||||
pub decision: IgnoreAudioDecision,
|
||||
pub event: Option<IgnoreAudioSourceResetEvent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum IgnoreAudioError {
|
||||
SampleRateOutOfRange { sample_rate_hz: u32 },
|
||||
TickPeriodOutOfRange { period_ns: u64 },
|
||||
BufferedFramesOutOfRange { buffered_frames: u64 },
|
||||
}
|
||||
|
||||
pub struct IgnoreAudioPolicy {
|
||||
sample_rate_hz: u32,
|
||||
metrics_by_source: BTreeMap<u64, IgnoreAudioMetrics>,
|
||||
}
|
||||
|
||||
impl IgnoreAudioPolicy {
|
||||
pub fn new(sample_rate_hz: u32) -> Result<Self, IgnoreAudioError> {
|
||||
if !(SAMPLE_RATE_HZ_MIN..=SAMPLE_RATE_HZ_MAX).contains(&sample_rate_hz) {
|
||||
return Err(IgnoreAudioError::SampleRateOutOfRange { sample_rate_hz });
|
||||
}
|
||||
assert!(sample_rate_hz >= SAMPLE_RATE_HZ_MIN);
|
||||
assert!(sample_rate_hz <= SAMPLE_RATE_HZ_MAX);
|
||||
Ok(Self {
|
||||
sample_rate_hz,
|
||||
metrics_by_source: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sample_rate_hz(&self) -> u32 {
|
||||
assert!(self.sample_rate_hz >= SAMPLE_RATE_HZ_MIN);
|
||||
assert!(self.sample_rate_hz <= SAMPLE_RATE_HZ_MAX);
|
||||
self.sample_rate_hz
|
||||
}
|
||||
|
||||
pub fn metrics_for(&self, source_id: u64) -> IgnoreAudioMetrics {
|
||||
assert!(self.sample_rate_hz >= SAMPLE_RATE_HZ_MIN);
|
||||
self.metrics_by_source
|
||||
.get(&source_id)
|
||||
.copied()
|
||||
.unwrap_or(IgnoreAudioMetrics::ZERO)
|
||||
}
|
||||
|
||||
pub fn evaluate(
|
||||
&mut self,
|
||||
source_state: &IgnoreAudioSourceState,
|
||||
tick: IgnoreAudioTick,
|
||||
) -> Result<IgnoreAudioEvaluation, IgnoreAudioError> {
|
||||
validate_source_state(source_state)?;
|
||||
validate_tick(tick)?;
|
||||
let tick_frames = compute_tick_frames(tick.period_ns, self.sample_rate_hz)?;
|
||||
assert!(tick_frames >= 1);
|
||||
let decision = decide(source_state, tick_frames);
|
||||
let event = self.apply_decision(source_state, tick, decision);
|
||||
let evaluation = IgnoreAudioEvaluation { decision, event };
|
||||
assert_evaluation_invariant(&evaluation);
|
||||
Ok(evaluation)
|
||||
}
|
||||
|
||||
fn apply_decision(
|
||||
&mut self,
|
||||
source_state: &IgnoreAudioSourceState,
|
||||
tick: IgnoreAudioTick,
|
||||
decision: IgnoreAudioDecision,
|
||||
) -> Option<IgnoreAudioSourceResetEvent> {
|
||||
assert!(self.sample_rate_hz >= SAMPLE_RATE_HZ_MIN);
|
||||
match decision {
|
||||
IgnoreAudioDecision::Mix => None,
|
||||
IgnoreAudioDecision::IgnoreThisTick => {
|
||||
let entry = self
|
||||
.metrics_by_source
|
||||
.entry(source_state.id)
|
||||
.or_insert(IgnoreAudioMetrics::ZERO);
|
||||
entry.ignored_tick_count = entry.ignored_tick_count.saturating_add(1);
|
||||
None
|
||||
}
|
||||
IgnoreAudioDecision::ResetSource => {
|
||||
let entry = self
|
||||
.metrics_by_source
|
||||
.entry(source_state.id)
|
||||
.or_insert(IgnoreAudioMetrics::ZERO);
|
||||
entry.reset_count = entry.reset_count.saturating_add(1);
|
||||
let reason = if source_state.last_frame_age_ns > SOURCE_STALE_AFTER_NS {
|
||||
IgnoreAudioResetReason::StaleSource
|
||||
} else {
|
||||
IgnoreAudioResetReason::BufferOverflow
|
||||
};
|
||||
Some(IgnoreAudioSourceResetEvent {
|
||||
source_id: source_state.id,
|
||||
at_ns: tick.at_ns,
|
||||
buffered_frames_at_reset: source_state.buffered_frames,
|
||||
last_frame_age_ns: source_state.last_frame_age_ns,
|
||||
reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_tick_frames(period_ns: u64, sample_rate_hz: u32) -> Result<u64, IgnoreAudioError> {
|
||||
if !(TICK_PERIOD_NS_MIN..=TICK_PERIOD_NS_MAX).contains(&period_ns) {
|
||||
return Err(IgnoreAudioError::TickPeriodOutOfRange { period_ns });
|
||||
}
|
||||
if !(SAMPLE_RATE_HZ_MIN..=SAMPLE_RATE_HZ_MAX).contains(&sample_rate_hz) {
|
||||
return Err(IgnoreAudioError::SampleRateOutOfRange { sample_rate_hz });
|
||||
}
|
||||
let product: u128 = (period_ns as u128) * (sample_rate_hz as u128);
|
||||
let frames = (product / 1_000_000_000u128) as u64;
|
||||
let frames = frames.max(1);
|
||||
assert!(frames >= 1);
|
||||
assert!(frames <= BUFFERED_FRAMES_MAX);
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
fn decide(source_state: &IgnoreAudioSourceState, tick_frames: u64) -> IgnoreAudioDecision {
|
||||
assert!(tick_frames >= 1);
|
||||
if source_state.is_muted {
|
||||
return IgnoreAudioDecision::Mix;
|
||||
}
|
||||
let reset_by_stale = source_state.last_frame_age_ns > SOURCE_STALE_AFTER_NS;
|
||||
let reset_by_buffer =
|
||||
source_state.buffered_frames > (SOURCE_RESET_AFTER_BUFFERED_TICKS as u64) * tick_frames;
|
||||
if reset_by_stale {
|
||||
return IgnoreAudioDecision::ResetSource;
|
||||
}
|
||||
if reset_by_buffer {
|
||||
return IgnoreAudioDecision::ResetSource;
|
||||
}
|
||||
if source_state.buffered_frames <= tick_frames {
|
||||
return IgnoreAudioDecision::Mix;
|
||||
}
|
||||
let over_buffering =
|
||||
source_state.buffered_frames > (AUDIO_BUFFERING_MAX_TICKS as u64) * tick_frames;
|
||||
if over_buffering {
|
||||
return IgnoreAudioDecision::IgnoreThisTick;
|
||||
}
|
||||
IgnoreAudioDecision::Mix
|
||||
}
|
||||
|
||||
fn validate_source_state(state: &IgnoreAudioSourceState) -> Result<(), IgnoreAudioError> {
|
||||
if state.buffered_frames > BUFFERED_FRAMES_MAX {
|
||||
return Err(IgnoreAudioError::BufferedFramesOutOfRange {
|
||||
buffered_frames: state.buffered_frames,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_tick(tick: IgnoreAudioTick) -> Result<(), IgnoreAudioError> {
|
||||
if !(TICK_PERIOD_NS_MIN..=TICK_PERIOD_NS_MAX).contains(&tick.period_ns) {
|
||||
return Err(IgnoreAudioError::TickPeriodOutOfRange {
|
||||
period_ns: tick.period_ns,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_evaluation_invariant(evaluation: &IgnoreAudioEvaluation) {
|
||||
match evaluation.decision {
|
||||
IgnoreAudioDecision::ResetSource => {
|
||||
assert!(evaluation.event.is_some());
|
||||
}
|
||||
_ => {
|
||||
assert!(evaluation.event.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn canonical_tick(at_ns: u64) -> IgnoreAudioTick {
|
||||
IgnoreAudioTick {
|
||||
at_ns,
|
||||
period_ns: 21_333_333,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sample_rate_below_min() {
|
||||
let err = IgnoreAudioPolicy::new(4_000).err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Some(IgnoreAudioError::SampleRateOutOfRange { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sample_rate_above_max() {
|
||||
let err = IgnoreAudioPolicy::new(500_000).err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Some(IgnoreAudioError::SampleRateOutOfRange { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tick_frames_at_48k_21_3ms_yields_1023_ish() {
|
||||
let frames = compute_tick_frames(21_333_333, 48_000).expect("ok");
|
||||
assert!(frames >= 1023);
|
||||
assert!(frames <= 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_buffer_yields_mix_decision() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 1,
|
||||
buffered_frames: 0,
|
||||
last_frame_age_ns: 0,
|
||||
is_muted: false,
|
||||
};
|
||||
let evaluation = policy.evaluate(&state, canonical_tick(0)).expect("ok");
|
||||
assert_eq!(evaluation.decision, IgnoreAudioDecision::Mix);
|
||||
assert!(evaluation.event.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn muted_source_always_yields_mix_decision() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 5,
|
||||
buffered_frames: 10_000_000,
|
||||
last_frame_age_ns: 0,
|
||||
is_muted: true,
|
||||
};
|
||||
let evaluation = policy.evaluate(&state, canonical_tick(0)).expect("ok");
|
||||
assert_eq!(evaluation.decision, IgnoreAudioDecision::Mix);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_buffered_yields_ignore_this_tick() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 9,
|
||||
buffered_frames: (AUDIO_BUFFERING_MAX_TICKS as u64) * 1024 + 1,
|
||||
last_frame_age_ns: 0,
|
||||
is_muted: false,
|
||||
};
|
||||
let evaluation = policy.evaluate(&state, canonical_tick(0)).expect("ok");
|
||||
assert_eq!(evaluation.decision, IgnoreAudioDecision::IgnoreThisTick);
|
||||
assert_eq!(policy.metrics_for(9).ignored_tick_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_threshold_buffer_triggers_reset() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 12,
|
||||
buffered_frames: (SOURCE_RESET_AFTER_BUFFERED_TICKS as u64) * 1024 + 1,
|
||||
last_frame_age_ns: 0,
|
||||
is_muted: false,
|
||||
};
|
||||
let evaluation = policy.evaluate(&state, canonical_tick(7)).expect("ok");
|
||||
assert_eq!(evaluation.decision, IgnoreAudioDecision::ResetSource);
|
||||
let event = evaluation.event.expect("reset emits event");
|
||||
assert_eq!(event.source_id, 12);
|
||||
assert_eq!(event.at_ns, 7);
|
||||
assert_eq!(event.reason, IgnoreAudioResetReason::BufferOverflow);
|
||||
assert_eq!(policy.metrics_for(12).reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_source_triggers_reset_with_stale_reason() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 1,
|
||||
buffered_frames: 0,
|
||||
last_frame_age_ns: SOURCE_STALE_AFTER_NS + 1,
|
||||
is_muted: false,
|
||||
};
|
||||
let evaluation = policy.evaluate(&state, canonical_tick(0)).expect("ok");
|
||||
assert_eq!(evaluation.decision, IgnoreAudioDecision::ResetSource);
|
||||
let event = evaluation.event.expect("reset emits event");
|
||||
assert_eq!(event.reason, IgnoreAudioResetReason::StaleSource);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn period_too_small_rejected() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 1,
|
||||
buffered_frames: 0,
|
||||
last_frame_age_ns: 0,
|
||||
is_muted: false,
|
||||
};
|
||||
let err = policy
|
||||
.evaluate(
|
||||
&state,
|
||||
IgnoreAudioTick {
|
||||
at_ns: 0,
|
||||
period_ns: 0,
|
||||
},
|
||||
)
|
||||
.err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Some(IgnoreAudioError::TickPeriodOutOfRange { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_source_age_uses_registration_for_never_pushed() {
|
||||
let registered_at_ns = 1_000_000;
|
||||
let now_ns = 1_000_000 + 6_000_000_000;
|
||||
let age = compute_source_age_ns(NEVER_PUSHED_SENTINEL, registered_at_ns, now_ns);
|
||||
assert_eq!(age, 6_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_source_age_uses_last_push_after_first_push() {
|
||||
let registered_at_ns = 1_000;
|
||||
let last_push_ns = 5_000;
|
||||
let age = compute_source_age_ns(last_push_ns, registered_at_ns, 9_000);
|
||||
assert_eq!(age, 4_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_source_age_zero_when_now_before_baseline() {
|
||||
let age = compute_source_age_ns(NEVER_PUSHED_SENTINEL, 5_000, 1_000);
|
||||
assert_eq!(age, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_accumulate_across_calls() {
|
||||
let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok");
|
||||
let state = IgnoreAudioSourceState {
|
||||
id: 2,
|
||||
buffered_frames: (AUDIO_BUFFERING_MAX_TICKS as u64) * 1024 + 1,
|
||||
last_frame_age_ns: 0,
|
||||
is_muted: false,
|
||||
};
|
||||
for _ in 0..5 {
|
||||
let _ = policy.evaluate(&state, canonical_tick(0));
|
||||
}
|
||||
assert_eq!(policy.metrics_for(2).ignored_tick_count, 5);
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
mod audio_contract;
|
||||
mod audio_mix_runtime;
|
||||
mod backend;
|
||||
mod direct_buffer;
|
||||
mod ignore_audio_runtime;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod audio_mix_runtime_bench_helpers {
|
||||
pub use crate::audio_mix_runtime::{
|
||||
AudioMixRuntime, AudioMixRuntimeBuilder, CaptureSource, CapturedMixOutputSink,
|
||||
MIX_CHANNELS, MIX_SAMPLE_RATE_HZ, MIX_TICK_PERIOD_NS, MixOutputFrame, MixOutputSink,
|
||||
MixRuntimeError, NullMixOutputSink, SOURCE_RING_CAP_FRAMES,
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod ignore_audio_bench_helpers {
|
||||
pub use crate::ignore_audio_runtime::{
|
||||
AUDIO_BUFFERING_MAX_TICKS, IgnoreAudioDecision, IgnoreAudioEvaluation, IgnoreAudioMetrics,
|
||||
IgnoreAudioPolicy, IgnoreAudioResetReason, IgnoreAudioSourceResetEvent,
|
||||
IgnoreAudioSourceState, IgnoreAudioTick, SOURCE_RESET_AFTER_BUFFERED_TICKS,
|
||||
SOURCE_STALE_AFTER_NS,
|
||||
};
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
mod pipewire;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -33,6 +13,8 @@ mod pipewire_bridge;
|
||||
mod routing;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod self_identity;
|
||||
#[cfg(all(test, target_os = "linux"))]
|
||||
mod test_alloc;
|
||||
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
@@ -42,9 +24,7 @@ use fluxer_screen_frame_bus::{NativeScreenFrameSinkHandle, NativeScreenFrameSink
|
||||
use napi::Env;
|
||||
use napi::JsValue;
|
||||
use napi::Status;
|
||||
use napi::bindgen_prelude::{
|
||||
Array, ArrayBuffer, Error, Function, Object, Result, Unknown, ValueType,
|
||||
};
|
||||
use napi::bindgen_prelude::{ArrayBuffer, Error, Function, Object, Result, Unknown, ValueType};
|
||||
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||
use napi_derive::napi;
|
||||
|
||||
@@ -52,9 +32,7 @@ use crate::audio_contract::{
|
||||
MAX_INVENTORY_FIELD_LENGTH, MAX_INVENTORY_FIELDS, MAX_ROUTING_RULE_KEY_LENGTH,
|
||||
MAX_ROUTING_RULE_KEYS_PER_PATTERN, MAX_ROUTING_RULE_PATTERNS, MAX_ROUTING_RULE_VALUE_LENGTH,
|
||||
};
|
||||
use crate::backend::{
|
||||
CaptureBridge as CaptureBridgeTrait, DirectCapture as DirectCaptureTrait, RoutingGraphSnapshot,
|
||||
};
|
||||
use crate::backend::{CaptureBridge as CaptureBridgeTrait, DirectCapture as DirectCaptureTrait};
|
||||
use crate::routing::{PropMap, PropPattern, RoutingRule, SelfIdentity};
|
||||
|
||||
type LifecycleTsfn =
|
||||
@@ -186,19 +164,6 @@ impl AudioBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi(js_name = "routingGraph")]
|
||||
pub fn routing_graph(&self) -> Result<RoutingGraphWire> {
|
||||
let guard = self
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|_| generic_error("AudioBridge backend poisoned"))?;
|
||||
let graph = guard
|
||||
.as_ref()
|
||||
.map(|b| b.routing_graph())
|
||||
.unwrap_or_default();
|
||||
Ok(RoutingGraphWire(graph))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn backend(&self) -> &'static str {
|
||||
self.name
|
||||
@@ -352,19 +317,6 @@ impl DirectAudioCapture {
|
||||
self.emit_lifecycle("closed-clean", "direct audio capture stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi(js_name = "routingGraph")]
|
||||
pub fn routing_graph(&self) -> Result<RoutingGraphWire> {
|
||||
let guard = self
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|_| generic_error("DirectAudioCapture backend poisoned"))?;
|
||||
let graph = guard
|
||||
.as_ref()
|
||||
.map(|b| b.routing_graph())
|
||||
.unwrap_or_default();
|
||||
Ok(RoutingGraphWire(graph))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DirectAudioCapture {
|
||||
@@ -390,139 +342,6 @@ impl DirectAudioCapture {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct AudioMixRuntimeHandle {
|
||||
inner: Mutex<Option<crate::audio_mix_runtime::AudioMixRuntime>>,
|
||||
source_count: u32,
|
||||
mark_pushed_total: Arc<std::sync::atomic::AtomicU64>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AudioMixRuntimeHandle {
|
||||
#[napi(constructor)]
|
||||
pub fn new(source_count: u32) -> Result<Self> {
|
||||
Self::build(source_count, None)
|
||||
}
|
||||
|
||||
#[napi(factory, js_name = "boundToDirectCapture")]
|
||||
pub fn bound_to_direct_capture(direct: &DirectAudioCapture) -> Result<Self> {
|
||||
let arc = direct_capture_freshness(direct)?;
|
||||
Self::build(1, Some(arc))
|
||||
}
|
||||
|
||||
fn build(
|
||||
source_count: u32,
|
||||
bound_freshness: Option<Arc<std::sync::atomic::AtomicU64>>,
|
||||
) -> Result<Self> {
|
||||
if source_count == 0 {
|
||||
return Err(invalid_arg(
|
||||
"AudioMixRuntimeHandle requires at least 1 source",
|
||||
));
|
||||
}
|
||||
if source_count as usize > fluxer_audio_mix::MAX_MIX_SOURCES {
|
||||
return Err(invalid_arg("AudioMixRuntimeHandle exceeds MAX_MIX_SOURCES"));
|
||||
}
|
||||
let clock: Arc<dyn fluxer_rt_thread::MonotonicClock> =
|
||||
Arc::new(fluxer_rt_thread::SystemMonotonicClock::new());
|
||||
let mut builder =
|
||||
crate::audio_mix_runtime::AudioMixRuntimeBuilder::new().with_clock(Arc::clone(&clock));
|
||||
for index in 0..source_count {
|
||||
let source_id = (index as u64) + 1;
|
||||
let (_source, consumer) = crate::audio_mix_runtime::CaptureSource::create(
|
||||
source_id,
|
||||
crate::audio_mix_runtime::MIX_SAMPLE_RATE_HZ,
|
||||
crate::audio_mix_runtime::MIX_CHANNELS,
|
||||
)
|
||||
.map_err(|_| generic_error("CaptureSource::create failed"))?;
|
||||
let freshness = if index == 0 {
|
||||
match &bound_freshness {
|
||||
Some(arc) => Arc::clone(arc),
|
||||
None => Arc::new(std::sync::atomic::AtomicU64::new(u64::MAX)),
|
||||
}
|
||||
} else {
|
||||
Arc::new(std::sync::atomic::AtomicU64::new(u64::MAX))
|
||||
};
|
||||
builder = builder.add_source_with_freshness(source_id, consumer, freshness);
|
||||
}
|
||||
let runtime = builder
|
||||
.build(crate::audio_mix_runtime::NullMixOutputSink)
|
||||
.map_err(|_| generic_error("AudioMixRuntimeBuilder::build failed"))?;
|
||||
let mark_pushed_total = runtime.mark_pushed_total_arc();
|
||||
Ok(Self {
|
||||
inner: Mutex::new(Some(runtime)),
|
||||
source_count,
|
||||
mark_pushed_total,
|
||||
})
|
||||
}
|
||||
|
||||
#[napi(js_name = "sourceCount")]
|
||||
pub fn source_count_js(&self) -> u32 {
|
||||
assert!(self.source_count > 0);
|
||||
assert!(self.source_count as usize <= fluxer_audio_mix::MAX_MIX_SOURCES);
|
||||
self.source_count
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn tick(&self, tick_at_ns: Option<i64>) -> Result<u32> {
|
||||
use fluxer_rt_thread::MonotonicClock as _;
|
||||
assert!(self.source_count > 0);
|
||||
let mut guard = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| generic_error("AudioMixRuntimeHandle poisoned"))?;
|
||||
let runtime = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| generic_error("AudioMixRuntimeHandle disposed"))?;
|
||||
let at_ns: u64 = match tick_at_ns {
|
||||
Some(v) if v > 0 => v as u64,
|
||||
_ => fluxer_rt_thread::SystemMonotonicClock::new().now_ns(),
|
||||
};
|
||||
assert!(at_ns > 0);
|
||||
let marked = runtime
|
||||
.observe_source_pushes_without_mix(at_ns)
|
||||
.map_err(|_| generic_error("AudioMixRuntime tick failed"))?;
|
||||
Ok(marked.min(u32::MAX as u64) as u32)
|
||||
}
|
||||
|
||||
#[napi(js_name = "markPushedTotal")]
|
||||
pub fn mark_pushed_total_js(&self) -> u32 {
|
||||
assert!(self.source_count > 0);
|
||||
let value = self
|
||||
.mark_pushed_total
|
||||
.load(std::sync::atomic::Ordering::Acquire);
|
||||
let clamped = value.min(u32::MAX as u64);
|
||||
assert!(clamped <= u32::MAX as u64);
|
||||
clamped as u32
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn dispose(&self) -> Result<()> {
|
||||
assert!(self.source_count > 0);
|
||||
assert!(self.source_count as usize <= fluxer_audio_mix::MAX_MIX_SOURCES);
|
||||
let mut guard = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| generic_error("AudioMixRuntimeHandle poisoned"))?;
|
||||
guard.take();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_capture_freshness(
|
||||
direct: &DirectAudioCapture,
|
||||
) -> Result<Arc<std::sync::atomic::AtomicU64>> {
|
||||
let guard = direct
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|_| generic_error("DirectAudioCapture backend poisoned"))?;
|
||||
let backend = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| generic_error("DirectAudioCapture backend unavailable"))?;
|
||||
backend
|
||||
.last_push_ns_arc()
|
||||
.ok_or_else(|| generic_error("DirectAudioCapture backend lacks freshness atomic"))
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct NativeAudioFrame<'env> {
|
||||
pub samples: ArrayBuffer<'env>,
|
||||
@@ -551,85 +370,6 @@ impl napi::bindgen_prelude::ToNapiValue for PropMapWire {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RoutingGraphWire(pub RoutingGraphSnapshot);
|
||||
|
||||
impl napi::bindgen_prelude::ToNapiValue for RoutingGraphWire {
|
||||
unsafe fn to_napi_value(
|
||||
raw_env: napi::sys::napi_env,
|
||||
value: Self,
|
||||
) -> Result<napi::sys::napi_value> {
|
||||
let env = napi::Env::from_raw(raw_env);
|
||||
let mut object = Object::new(&env)?;
|
||||
object.set("backend", value.0.backend)?;
|
||||
object.set("nodes", routing_graph_nodes_to_array(&env, value.0.nodes)?)?;
|
||||
object.set("ports", routing_graph_ports_to_array(&env, value.0.ports)?)?;
|
||||
object.set(
|
||||
"ownedLinks",
|
||||
routing_graph_links_to_array(&env, value.0.owned_links)?,
|
||||
)?;
|
||||
unsafe {
|
||||
<Object<'_> as napi::bindgen_prelude::ToNapiValue>::to_napi_value(raw_env, object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prop_map_to_object<'env>(env: &'env Env, props: PropMap) -> Result<Object<'env>> {
|
||||
let mut object = Object::new(env)?;
|
||||
for (key, value) in props {
|
||||
object.set(&key, value)?;
|
||||
}
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
fn routing_graph_nodes_to_array<'env>(
|
||||
env: &'env Env,
|
||||
nodes: Vec<crate::backend::RoutingGraphNode>,
|
||||
) -> Result<Array<'env>> {
|
||||
let mut array = env.create_array(nodes.len() as u32)?;
|
||||
for (index, node) in nodes.into_iter().enumerate() {
|
||||
let mut object = Object::new(env)?;
|
||||
object.set("id", node.id)?;
|
||||
object.set("props", prop_map_to_object(env, node.props)?)?;
|
||||
array.set(index as u32, object)?;
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn routing_graph_ports_to_array<'env>(
|
||||
env: &'env Env,
|
||||
ports: Vec<crate::backend::RoutingGraphPort>,
|
||||
) -> Result<Array<'env>> {
|
||||
let mut array = env.create_array(ports.len() as u32)?;
|
||||
for (index, port) in ports.into_iter().enumerate() {
|
||||
let mut object = Object::new(env)?;
|
||||
object.set("id", port.id)?;
|
||||
object.set("nodeId", port.node_id)?;
|
||||
object.set("direction", port.direction)?;
|
||||
object.set("channel", port.channel)?;
|
||||
object.set("props", prop_map_to_object(env, port.props)?)?;
|
||||
array.set(index as u32, object)?;
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn routing_graph_links_to_array<'env>(
|
||||
env: &'env Env,
|
||||
links: Vec<crate::backend::RoutingGraphLink>,
|
||||
) -> Result<Array<'env>> {
|
||||
let mut array = env.create_array(links.len() as u32)?;
|
||||
for (index, link) in links.into_iter().enumerate() {
|
||||
let mut object = Object::new(env)?;
|
||||
object.set("outputNodeId", link.output_node_id)?;
|
||||
object.set("outputPortId", link.output_port_id)?;
|
||||
object.set("inputNodeId", link.input_node_id)?;
|
||||
object.set("inputPortId", link.input_port_id)?;
|
||||
object.set("owned", true)?;
|
||||
object.set("passive", true)?;
|
||||
array.set(index as u32, object)?;
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn project_inventory_entry(mut entry: PropMap, fields: &[String]) -> PropMapWire {
|
||||
if fields.is_empty() {
|
||||
return PropMapWire(entry);
|
||||
@@ -759,104 +499,3 @@ fn invalid_arg(reason: impl Into<String>) -> Error {
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _keep_arc_in_scope(_: Arc<()>) {}
|
||||
|
||||
#[cfg(all(test, target_os = "linux"))]
|
||||
mod js_path_tests {
|
||||
use super::AudioMixRuntimeHandle;
|
||||
use crate::pipewire::stream_ops::{
|
||||
DIRECT_CAPTURE_APM_FRAME_SAMPLES, build_test_user_data, process_audio_chunk,
|
||||
};
|
||||
use fluxer_rt_thread::MonotonicClock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FakeClock {
|
||||
value_ns: AtomicU64,
|
||||
}
|
||||
|
||||
impl FakeClock {
|
||||
fn new(initial_ns: u64) -> Self {
|
||||
assert!(initial_ns > 0);
|
||||
Self {
|
||||
value_ns: AtomicU64::new(initial_ns),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MonotonicClock for FakeClock {
|
||||
fn now_ns(&self) -> u64 {
|
||||
self.value_ns.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
fn make_f32_payload(samples: &[f32]) -> Vec<u8> {
|
||||
assert!(!samples.is_empty());
|
||||
let mut out = Vec::with_capacity(samples.len() * 4);
|
||||
for sample in samples {
|
||||
out.extend_from_slice(&sample.to_ne_bytes());
|
||||
}
|
||||
assert_eq!(out.len(), samples.len() * 4);
|
||||
out
|
||||
}
|
||||
|
||||
fn build_handle_with_shared_freshness(last_push_ns: Arc<AtomicU64>) -> AudioMixRuntimeHandle {
|
||||
assert!(Arc::strong_count(&last_push_ns) >= 1);
|
||||
let handle = AudioMixRuntimeHandle::build(1, Some(last_push_ns))
|
||||
.expect("AudioMixRuntimeHandle build via JS path");
|
||||
assert_eq!(handle.source_count_js(), 1);
|
||||
handle
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_runtime_tick_consumes_freshness_pushed_by_production_callback() {
|
||||
let clock: Arc<dyn MonotonicClock> = Arc::new(FakeClock::new(11_000_000));
|
||||
let last_push_ns = Arc::new(AtomicU64::new(u64::MAX));
|
||||
let mut user_data = build_test_user_data(Arc::clone(&last_push_ns), Arc::clone(&clock));
|
||||
let handle = build_handle_with_shared_freshness(Arc::clone(&last_push_ns));
|
||||
assert_eq!(handle.mark_pushed_total_js(), 0);
|
||||
assert_eq!(last_push_ns.load(Ordering::Acquire), u64::MAX);
|
||||
let frame: Vec<f32> = (0..DIRECT_CAPTURE_APM_FRAME_SAMPLES)
|
||||
.map(|n| (n as f32) * 0.0001)
|
||||
.collect();
|
||||
let payload = make_f32_payload(&frame);
|
||||
process_audio_chunk(&mut user_data, &payload);
|
||||
let pushed_after_callback = last_push_ns.load(Ordering::Acquire);
|
||||
assert_ne!(pushed_after_callback, u64::MAX);
|
||||
assert_eq!(pushed_after_callback, 11_000_000);
|
||||
let marked = handle
|
||||
.tick(Some(pushed_after_callback as i64))
|
||||
.expect("AudioMixRuntimeHandle::tick observes freshness");
|
||||
assert_eq!(marked, 1);
|
||||
let total = handle.mark_pushed_total_js();
|
||||
assert!(
|
||||
total >= 1,
|
||||
"AudioMixRuntimeHandle::tick did not advance mark_pushed_total ({total})",
|
||||
);
|
||||
handle.dispose().expect("dispose");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_runtime_tick_idempotent_for_unchanged_freshness_atomic() {
|
||||
let clock: Arc<dyn MonotonicClock> = Arc::new(FakeClock::new(22_000_000));
|
||||
let last_push_ns = Arc::new(AtomicU64::new(u64::MAX));
|
||||
let mut user_data = build_test_user_data(Arc::clone(&last_push_ns), Arc::clone(&clock));
|
||||
let handle = build_handle_with_shared_freshness(Arc::clone(&last_push_ns));
|
||||
let frame: Vec<f32> = (0..DIRECT_CAPTURE_APM_FRAME_SAMPLES)
|
||||
.map(|n| (n as f32) * 0.0002)
|
||||
.collect();
|
||||
let payload = make_f32_payload(&frame);
|
||||
process_audio_chunk(&mut user_data, &payload);
|
||||
let observed = last_push_ns.load(Ordering::Acquire);
|
||||
let _ = handle.tick(Some(observed as i64)).expect("first tick");
|
||||
let after_first = handle.mark_pushed_total_js();
|
||||
assert!(after_first >= 1);
|
||||
let _ = handle.tick(Some(observed as i64 + 1)).expect("second tick");
|
||||
let after_second = handle.mark_pushed_total_js();
|
||||
assert_eq!(
|
||||
after_first, after_second,
|
||||
"second tick must not advance mark_pushed_total when freshness atomic is unchanged",
|
||||
);
|
||||
handle.dispose().expect("dispose");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ use pw::properties::{PropertiesBox, properties};
|
||||
use fluxer_rt_thread::{PriorityProfile, RealtimePriorityGuard, RtError, RtOutcome};
|
||||
|
||||
use crate::audio_contract::{self, DIRECT_CAPTURE_SAMPLE_RATE};
|
||||
use crate::backend::{RoutingGraphLink, RoutingGraphNode, RoutingGraphPort, RoutingGraphSnapshot};
|
||||
use crate::routing::PropMap;
|
||||
|
||||
pub(crate) const READY_TIMEOUT_MS: u64 = 2_000;
|
||||
@@ -130,32 +129,6 @@ impl InventorySnapshot {
|
||||
.map(|props| self.enriched_node_props(props))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn routing_graph_nodes(&self) -> Vec<RoutingGraphNode> {
|
||||
let mut nodes: Vec<RoutingGraphNode> = self
|
||||
.enriched_nodes()
|
||||
.into_iter()
|
||||
.map(|(id, props)| RoutingGraphNode { id, props })
|
||||
.collect();
|
||||
nodes.sort_by_key(|node| node.id);
|
||||
nodes
|
||||
}
|
||||
|
||||
pub(crate) fn routing_graph_ports(&self) -> Vec<RoutingGraphPort> {
|
||||
let mut ports: Vec<RoutingGraphPort> = self
|
||||
.ports
|
||||
.iter()
|
||||
.map(|(id, port)| RoutingGraphPort {
|
||||
id: *id,
|
||||
node_id: port.node_id,
|
||||
direction: port.direction.clone(),
|
||||
channel: port.channel.clone(),
|
||||
props: port.props.clone(),
|
||||
})
|
||||
.collect();
|
||||
ports.sort_by_key(|port| port.id);
|
||||
ports
|
||||
}
|
||||
}
|
||||
|
||||
fn client_identity_props(client: &PropMap) -> PropMap {
|
||||
@@ -307,15 +280,6 @@ impl LinkKey {
|
||||
sink_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn graph_link(self) -> RoutingGraphLink {
|
||||
RoutingGraphLink {
|
||||
output_node_id: self.src_node,
|
||||
output_port_id: self.src_port,
|
||||
input_node_id: self.sink_node,
|
||||
input_port_id: self.sink_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct OwnedLink {
|
||||
@@ -400,27 +364,6 @@ pub(crate) fn ensure_virtual_sink(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_routing_graph_snapshot(
|
||||
backend: &str,
|
||||
inventory: &Arc<Mutex<InventorySnapshot>>,
|
||||
owned_link_snapshot: &Arc<Mutex<Vec<LinkKey>>>,
|
||||
) -> RoutingGraphSnapshot {
|
||||
let (nodes, ports) = match inventory.lock() {
|
||||
Ok(guard) => (guard.routing_graph_nodes(), guard.routing_graph_ports()),
|
||||
Err(_) => (Vec::new(), Vec::new()),
|
||||
};
|
||||
let owned_links = match owned_link_snapshot.lock() {
|
||||
Ok(guard) => guard.iter().copied().map(LinkKey::graph_link).collect(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
RoutingGraphSnapshot {
|
||||
backend: backend.to_string(),
|
||||
nodes,
|
||||
ports,
|
||||
owned_links,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn daemon_reachable() -> bool {
|
||||
pw::init();
|
||||
let Ok(mainloop) = pw::main_loop::MainLoopRc::new(None) else {
|
||||
|
||||
@@ -12,11 +12,11 @@ use pw::channel::{Sender as PwSender, channel as pw_channel};
|
||||
use fluxer_rt_thread::{MonotonicClock, SystemMonotonicClock};
|
||||
|
||||
use crate::audio_contract::DIRECT_CAPTURE_MAX_READ_SAMPLES;
|
||||
use crate::backend::{CaptureBridge, CapturedFrame, DirectCapture, RoutingGraphSnapshot};
|
||||
use crate::backend::{CaptureBridge, CapturedFrame, DirectCapture};
|
||||
use crate::direct_buffer::DirectAudioBuffer;
|
||||
use crate::pipewire::common::{
|
||||
InventorySnapshot, LinkKey, READY_TIMEOUT_MS, build_routing_graph_snapshot,
|
||||
daemon_reachable as common_daemon_reachable, next_direct_sink_name,
|
||||
InventorySnapshot, READY_TIMEOUT_MS, daemon_reachable as common_daemon_reachable,
|
||||
next_direct_sink_name,
|
||||
};
|
||||
use crate::pipewire::event_loop::{
|
||||
BridgeCommand, DirectCommand, DirectWorkerInputs, run_bridge_worker, run_direct_worker,
|
||||
@@ -30,7 +30,6 @@ pub fn daemon_reachable() -> bool {
|
||||
|
||||
pub struct PipeWireBridge {
|
||||
snapshot: Arc<Mutex<InventorySnapshot>>,
|
||||
owned_link_snapshot: Arc<Mutex<Vec<LinkKey>>>,
|
||||
tx: PwSender<BridgeCommand>,
|
||||
thread: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
@@ -44,18 +43,16 @@ impl PipeWireBridge {
|
||||
let owned_link_snapshot = Arc::new(Mutex::new(Vec::new()));
|
||||
let (tx, rx) = pw_channel::<BridgeCommand>();
|
||||
let snap_for_thread = snapshot.clone();
|
||||
let links_for_thread = owned_link_snapshot.clone();
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<bool>(1);
|
||||
let handle = thread::Builder::new()
|
||||
.name("fluxer-pipewire-bridge".into())
|
||||
.spawn(move || {
|
||||
run_bridge_worker(snap_for_thread, links_for_thread, rx, ready_tx);
|
||||
run_bridge_worker(snap_for_thread, owned_link_snapshot, rx, ready_tx);
|
||||
})
|
||||
.ok()?;
|
||||
match ready_rx.recv_timeout(Duration::from_millis(READY_TIMEOUT_MS)) {
|
||||
Ok(true) => Some(Self {
|
||||
snapshot,
|
||||
owned_link_snapshot,
|
||||
tx,
|
||||
thread: Mutex::new(Some(handle)),
|
||||
}),
|
||||
@@ -102,16 +99,10 @@ impl CaptureBridge for PipeWireBridge {
|
||||
fn backend_name(&self) -> &'static str {
|
||||
"pipewire"
|
||||
}
|
||||
|
||||
fn routing_graph(&self) -> RoutingGraphSnapshot {
|
||||
build_routing_graph_snapshot("pipewire", &self.snapshot, &self.owned_link_snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PipeWireDirectCapture {
|
||||
samples: Arc<Mutex<DirectAudioBuffer>>,
|
||||
inventory: Arc<Mutex<InventorySnapshot>>,
|
||||
owned_link_snapshot: Arc<Mutex<Vec<LinkKey>>>,
|
||||
tx: PwSender<DirectCommand>,
|
||||
#[allow(dead_code)]
|
||||
running: Arc<AtomicBool>,
|
||||
@@ -141,8 +132,8 @@ impl PipeWireDirectCapture {
|
||||
let sink_node_name = next_direct_sink_name();
|
||||
let inputs = DirectWorkerInputs {
|
||||
samples: samples.clone(),
|
||||
inventory: inventory.clone(),
|
||||
owned_link_snapshot: owned_link_snapshot.clone(),
|
||||
inventory,
|
||||
owned_link_snapshot,
|
||||
running: running.clone(),
|
||||
sink_node_name,
|
||||
last_push_ns: last_push_ns.clone(),
|
||||
@@ -159,8 +150,6 @@ impl PipeWireDirectCapture {
|
||||
match ready_rx.recv_timeout(Duration::from_millis(READY_TIMEOUT_MS)) {
|
||||
Ok(true) => Some(Self {
|
||||
samples,
|
||||
inventory,
|
||||
owned_link_snapshot,
|
||||
tx,
|
||||
running,
|
||||
thread: Mutex::new(Some(handle)),
|
||||
@@ -247,14 +236,6 @@ impl DirectCapture for PipeWireDirectCapture {
|
||||
*guard = identity;
|
||||
}
|
||||
}
|
||||
|
||||
fn routing_graph(&self) -> RoutingGraphSnapshot {
|
||||
build_routing_graph_snapshot("pipewire", &self.inventory, &self.owned_link_snapshot)
|
||||
}
|
||||
|
||||
fn last_push_ns_arc(&self) -> Option<Arc<AtomicU64>> {
|
||||
Some(Arc::clone(&self.last_push_ns))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -793,7 +774,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::ignore_audio_runtime::SOURCE_STALE_AFTER_NS;
|
||||
use crate::pipewire::stream_ops::{build_test_user_data, process_audio_chunk};
|
||||
use fluxer_rt_thread::MonotonicClock;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -828,49 +808,6 @@ mod tests {
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_callback_marks_freshness_with_monotonic_clock() {
|
||||
let clock = Arc::new(FakeClock::new(7_500_000));
|
||||
let last_push_ns = Arc::new(AtomicU64::new(u64::MAX));
|
||||
let mut data = build_test_user_data(
|
||||
last_push_ns.clone(),
|
||||
Arc::clone(&clock) as Arc<dyn MonotonicClock>,
|
||||
);
|
||||
assert_eq!(last_push_ns.load(Ordering::Acquire), u64::MAX);
|
||||
let frame: Vec<f32> = (0..960).map(|n| (n as f32) * 0.0001).collect();
|
||||
let payload = make_f32_payload(&frame);
|
||||
process_audio_chunk(&mut data, &payload);
|
||||
let observed = last_push_ns.load(Ordering::Acquire);
|
||||
assert_eq!(observed, 7_500_000);
|
||||
assert_ne!(observed, u64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freshness_age_grows_to_signal_stale_source_after_threshold() {
|
||||
let clock = Arc::new(FakeClock::new(1_000_000));
|
||||
let last_push_ns = Arc::new(AtomicU64::new(u64::MAX));
|
||||
let mut data = build_test_user_data(
|
||||
last_push_ns.clone(),
|
||||
Arc::clone(&clock) as Arc<dyn MonotonicClock>,
|
||||
);
|
||||
let frame = vec![0.1_f32; DIRECT_CAPTURE_APM_FRAME_SAMPLES];
|
||||
let payload = make_f32_payload(&frame);
|
||||
process_audio_chunk(&mut data, &payload);
|
||||
let after_first = last_push_ns.load(Ordering::Acquire);
|
||||
assert_eq!(after_first, 1_000_000);
|
||||
clock.set(1_000_000 + SOURCE_STALE_AFTER_NS + 1);
|
||||
let age = clock.now_ns() - after_first;
|
||||
assert!(age > SOURCE_STALE_AFTER_NS);
|
||||
clock.set(2_000_000 + SOURCE_STALE_AFTER_NS + 1);
|
||||
let payload2 = make_f32_payload(&frame);
|
||||
process_audio_chunk(&mut data, &payload2);
|
||||
let after_second = last_push_ns.load(Ordering::Acquire);
|
||||
assert!(after_second > after_first);
|
||||
assert_eq!(after_second, 2_000_000 + SOURCE_STALE_AFTER_NS + 1);
|
||||
let fresh_age = clock.now_ns() - after_second;
|
||||
assert_eq!(fresh_age, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_path_does_not_allocate_in_steady_state() {
|
||||
let clock = Arc::new(FakeClock::new(1_000));
|
||||
@@ -885,12 +822,12 @@ mod tests {
|
||||
clock.set(clock.now_ns() + 10_000_000);
|
||||
process_audio_chunk(&mut data, &payload);
|
||||
}
|
||||
let allocs_before = crate::audio_mix_runtime::ALLOC_PROBE.load(Ordering::Relaxed);
|
||||
crate::audio_mix_runtime::begin_thread_alloc_probe();
|
||||
let allocs_before = crate::test_alloc::ALLOC_PROBE.load(Ordering::Relaxed);
|
||||
crate::test_alloc::begin_thread_alloc_probe();
|
||||
clock.set(clock.now_ns() + 10_000_000);
|
||||
process_audio_chunk(&mut data, &payload);
|
||||
let probed = crate::audio_mix_runtime::end_thread_alloc_probe();
|
||||
let allocs_after = crate::audio_mix_runtime::ALLOC_PROBE.load(Ordering::Relaxed);
|
||||
let probed = crate::test_alloc::end_thread_alloc_probe();
|
||||
let allocs_after = crate::test_alloc::ALLOC_PROBE.load(Ordering::Relaxed);
|
||||
assert_eq!(
|
||||
probed,
|
||||
0,
|
||||
@@ -898,45 +835,4 @@ mod tests {
|
||||
allocs_after.saturating_sub(allocs_before)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_callback_freshness_drives_audio_mix_runtime_mark_pushed() {
|
||||
use crate::audio_mix_runtime::{
|
||||
AudioMixRuntimeBuilder, CaptureSource, MIX_CHANNELS, MIX_SAMPLE_RATE_HZ,
|
||||
NullMixOutputSink,
|
||||
};
|
||||
let clock = Arc::new(FakeClock::new(9_000_000));
|
||||
let last_push_ns = Arc::new(AtomicU64::new(u64::MAX));
|
||||
let mut data = build_test_user_data(
|
||||
last_push_ns.clone(),
|
||||
Arc::clone(&clock) as Arc<dyn MonotonicClock>,
|
||||
);
|
||||
let source_id: u64 = 1;
|
||||
let (_source, consumer) =
|
||||
CaptureSource::create(source_id, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS).expect("source");
|
||||
let mut runtime = AudioMixRuntimeBuilder::new()
|
||||
.with_clock(Arc::clone(&clock) as Arc<dyn MonotonicClock>)
|
||||
.add_source_with_freshness(source_id, consumer, Arc::clone(&last_push_ns))
|
||||
.build(NullMixOutputSink)
|
||||
.expect("build");
|
||||
assert_eq!(runtime.mark_pushed_total(), 0);
|
||||
assert_eq!(last_push_ns.load(Ordering::Acquire), u64::MAX);
|
||||
let frame: Vec<f32> = (0..DIRECT_CAPTURE_APM_FRAME_SAMPLES)
|
||||
.map(|n| (n as f32) * 0.0001)
|
||||
.collect();
|
||||
let payload = make_f32_payload(&frame);
|
||||
process_audio_chunk(&mut data, &payload);
|
||||
let observed = last_push_ns.load(Ordering::Acquire);
|
||||
assert_eq!(observed, 9_000_000);
|
||||
let _ = runtime.run_one_tick_blocking(observed).expect("frame");
|
||||
assert!(
|
||||
runtime.mark_pushed_total() >= 1,
|
||||
"AudioMixRuntime.tick() did not invoke StaleSourceTracker::mark_pushed",
|
||||
);
|
||||
let not_stale = !runtime.is_source_stale(0, observed + 1_000_000, 5_000_000_000);
|
||||
assert!(
|
||||
not_stale,
|
||||
"source must not be stale immediately after a fresh push"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub(crate) static ALLOC_PROBE: std::sync::atomic::AtomicUsize =
|
||||
std::sync::atomic::AtomicUsize::new(0);
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static THREAD_ALLOC_TRACK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
pub(crate) static THREAD_ALLOC_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
struct CountingAllocator;
|
||||
|
||||
unsafe impl std::alloc::GlobalAlloc for CountingAllocator {
|
||||
unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
|
||||
ALLOC_PROBE.fetch_add(1, Ordering::Relaxed);
|
||||
let _ = THREAD_ALLOC_TRACK.try_with(|tracked| {
|
||||
if tracked.get() {
|
||||
let _ = THREAD_ALLOC_COUNT.try_with(|c| c.set(c.get().saturating_add(1)));
|
||||
}
|
||||
});
|
||||
unsafe { std::alloc::System.alloc(layout) }
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
|
||||
unsafe { std::alloc::System.dealloc(ptr, layout) }
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: CountingAllocator = CountingAllocator;
|
||||
|
||||
pub(crate) fn begin_thread_alloc_probe() {
|
||||
THREAD_ALLOC_COUNT.with(|c| c.set(0));
|
||||
THREAD_ALLOC_TRACK.with(|t| t.set(true));
|
||||
}
|
||||
|
||||
pub(crate) fn end_thread_alloc_probe() -> usize {
|
||||
THREAD_ALLOC_TRACK.with(|t| t.set(false));
|
||||
THREAD_ALLOC_COUNT.with(|c| c.get())
|
||||
}
|
||||
Reference in new issue
Block a user