use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use mineral_audio::{AudioHandle, SpectrumTap};
use mineral_protocol::PcmChunk;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use tokio::sync::mpsc;
pub(crate) const PCM_TICK_MS: u64 = 16;
const MAX_HISTORY: usize = 16 * 1024;
const MAX_CHUNK: usize = 2048;
pub(crate) const SUBSCRIBER_QUEUE_CHUNKS: usize = 8;
const POSITION_JUMP_MS: u64 = 1000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct PcmSubscription(u64);
impl PcmSubscription {
#[cfg(test)]
pub(crate) fn value(self) -> u64 {
self.0
}
}
struct Subscriber {
next_position: u64,
gap_pending: bool,
tx: mpsc::Sender<PcmChunk>,
}
struct RelayState {
consumed: u64,
generation: u64,
track_token: u64,
sample_rate: Option<u32>,
expected_position_ms: u64,
history: VecDeque<f32>,
subscribers: FxHashMap<u64, Subscriber>,
next_subscription: u64,
}
impl RelayState {
fn bump_generation(&mut self) {
self.generation = self.generation.saturating_add(1);
self.history.clear();
for subscriber in self.subscribers.values_mut() {
subscriber.gap_pending = true;
subscriber.next_position = self.consumed;
}
}
fn ingest(&mut self, samples: &[f32]) {
self.history.extend(samples.iter().copied());
self.consumed = self
.consumed
.saturating_add(u64::try_from(samples.len()).unwrap_or(u64::MAX));
let overflow = self.history.len().saturating_sub(MAX_HISTORY);
if overflow > 0 {
self.history.drain(..overflow);
}
}
}
struct RelayInput {
track_token: u64,
position_ms: u64,
playing: bool,
sample_rate: u32,
pushed_total: u64,
samples: Vec<f32>,
}
#[derive(Clone)]
pub(crate) struct PcmRelay {
inner: Arc<Mutex<RelayState>>,
}
impl PcmRelay {
pub(crate) fn spawn(tap: SpectrumTap, audio: AudioHandle) -> Self {
let relay = Self {
inner: Arc::new(Mutex::new(RelayState {
consumed: 0,
generation: 1,
track_token: 0,
sample_rate: None,
expected_position_ms: 0,
history: VecDeque::new(),
subscribers: FxHashMap::default(),
next_subscription: 0,
})),
};
let pump = relay.clone();
tokio::spawn(async move { pump.run(audio, tap).await });
relay
}
#[cfg(test)]
pub(crate) fn detached_for_test() -> Self {
Self {
inner: Arc::new(Mutex::new(RelayState {
consumed: 0,
generation: 1,
track_token: 0,
sample_rate: None,
expected_position_ms: 0,
history: VecDeque::new(),
subscribers: FxHashMap::default(),
next_subscription: 0,
})),
}
}
pub(crate) fn subscribe(&self) -> (PcmSubscription, mpsc::Receiver<PcmChunk>) {
let mut state = self.inner.lock();
let token = state.next_subscription;
state.next_subscription = state.next_subscription.wrapping_add(1);
let (tx, rx) = mpsc::channel(SUBSCRIBER_QUEUE_CHUNKS);
let window_start = state
.consumed
.saturating_sub(u64::try_from(state.history.len()).unwrap_or(u64::MAX));
state.subscribers.insert(
token,
Subscriber {
next_position: window_start,
gap_pending: true,
tx,
},
);
(PcmSubscription(token), rx)
}
pub(crate) fn unsubscribe(&self, token: PcmSubscription) {
self.inner.lock().subscribers.remove(&token.0);
}
#[cfg(test)]
pub(crate) fn subscriber_count(&self) -> usize {
self.inner.lock().subscribers.len()
}
#[cfg(test)]
pub(crate) fn generation(&self) -> u64 {
self.inner.lock().generation
}
async fn run(self, audio: AudioHandle, mut tap: SpectrumTap) {
let mut tick = tokio::time::interval(Duration::from_millis(PCM_TICK_MS));
let mut scratch = [0_f32; 4096];
loop {
tick.tick().await;
let snapshot = audio.snapshot();
let mut samples = Vec::new();
loop {
let got = tap.pop_into(&mut scratch);
if got == 0 {
break;
}
if let Some(batch) = scratch.get(..got) {
samples.extend_from_slice(batch);
}
if got < scratch.len() {
break;
}
}
self.pump(&RelayInput {
track_token: snapshot.current_track_token,
position_ms: snapshot.position_ms,
playing: snapshot.playing,
sample_rate: tap.sample_rate(),
pushed_total: tap.pushed_total(),
samples,
});
}
}
fn pump(&self, input: &RelayInput) {
let mut state = self.inner.lock();
if input.track_token != state.track_token {
state.track_token = input.track_token;
state.bump_generation();
state.expected_position_ms = input.position_ms;
}
if input.sample_rate != 0 {
match state.sample_rate {
Some(previous) if previous != input.sample_rate => {
state.sample_rate = Some(input.sample_rate);
state.bump_generation();
}
None => state.sample_rate = Some(input.sample_rate),
Some(_) => {}
}
}
let mut drained = 0_u64;
for batch in input.samples.chunks(4096) {
state.ingest(batch);
drained = drained.saturating_add(u64::try_from(batch.len()).unwrap_or(u64::MAX));
}
let dropped_upstream = input.pushed_total.saturating_sub(state.consumed);
if dropped_upstream > 0 {
state.consumed = input.pushed_total;
for subscriber in state.subscribers.values_mut() {
subscriber.gap_pending = true;
}
}
if drained > 0 {
let rate = state.sample_rate.unwrap_or(0);
if rate > 0 {
state.expected_position_ms = state
.expected_position_ms
.saturating_add(drained.saturating_mul(1000) / u64::from(rate));
}
}
if input.playing
&& rate_known(&state)
&& input.position_ms.abs_diff(state.expected_position_ms) > POSITION_JUMP_MS
{
state.expected_position_ms = input.position_ms;
state.bump_generation();
}
if state.subscribers.is_empty() {
return;
}
let sample_rate = state.sample_rate;
let generation = state.generation;
let consumed = state.consumed;
let history = std::mem::take(&mut state.history);
let window_start =
consumed.saturating_sub(u64::try_from(history.len()).unwrap_or(u64::MAX));
let mut to_drop = Vec::new();
for (token, subscriber) in state.subscribers.iter_mut() {
if subscriber.next_position < window_start {
subscriber.next_position = window_start;
subscriber.gap_pending = true;
}
let available = consumed.saturating_sub(subscriber.next_position);
if available == 0 {
continue;
}
let take = usize::try_from(available)
.unwrap_or(usize::MAX)
.min(MAX_CHUNK);
let offset =
usize::try_from(subscriber.next_position.saturating_sub(window_start)).unwrap_or(0);
let samples = history
.iter()
.skip(offset)
.take(take)
.copied()
.collect::<Vec<f32>>();
let chunk = PcmChunk {
generation,
position: subscriber.next_position,
gap: subscriber.gap_pending,
sample_rate,
samples,
};
let len = u64::try_from(chunk.samples.len()).unwrap_or(u64::MAX);
match subscriber.tx.try_send(chunk) {
Ok(()) => {
subscriber.next_position = subscriber.next_position.saturating_add(len);
subscriber.gap_pending = false;
}
Err(mpsc::error::TrySendError::Full(_)) => {
subscriber.gap_pending = true;
}
Err(mpsc::error::TrySendError::Closed(_)) => {
to_drop.push(*token);
}
}
}
for token in to_drop {
state.subscribers.remove(&token);
}
state.history = history;
}
}
fn rate_known(state: &RelayState) -> bool {
state.sample_rate.is_some_and(|rate| rate > 0)
}
#[cfg(test)]
mod tests {
use tokio::sync::mpsc;
use super::{MAX_CHUNK, MAX_HISTORY, PcmChunk, PcmRelay, RelayInput, SUBSCRIBER_QUEUE_CHUNKS};
fn relay() -> PcmRelay {
PcmRelay::detached_for_test()
}
fn input(samples: usize) -> RelayInput {
RelayInput {
track_token: 1,
position_ms: 0,
playing: true,
sample_rate: 44_100,
pushed_total: 0,
samples: vec![0.25; samples],
}
}
fn drain(rx: &mut mpsc::Receiver<PcmChunk>) -> Vec<PcmChunk> {
let mut out = Vec::new();
while let Ok(chunk) = rx.try_recv() {
out.push(chunk);
}
out
}
#[test]
fn subscriber_lifecycle_is_balanced() {
let relay = relay();
assert_eq!(relay.subscriber_count(), 0);
assert_eq!(relay.generation(), 1);
let (token, _rx) = relay.subscribe();
assert_eq!(token.value(), 0);
assert_eq!(relay.subscriber_count(), 1);
relay.unsubscribe(token);
assert_eq!(relay.subscriber_count(), 0);
}
#[test]
fn new_subscriber_starts_with_gap_then_streams() {
let relay = relay();
let (token, mut rx) = relay.subscribe();
relay.pump(&input( 3000));
relay.pump(&input( 0));
let chunks = drain(&mut rx);
assert!(!chunks.is_empty(), "应收到样本块");
assert!(
chunks.first().is_some_and(|chunk| chunk.gap),
"首块应标缺口"
);
assert!(
chunks.iter().skip(1).all(|chunk| !chunk.gap),
"后续块不应标缺口"
);
let total: usize = chunks.iter().map(|chunk| chunk.samples.len()).sum();
assert_eq!(total, 3000);
assert!(
chunks
.windows(2)
.all(|pair| match (pair.first(), pair.get(1)) {
(Some(left), Some(right)) => right.position > left.position,
_ => false,
}),
"块位置应严格递增"
);
relay.unsubscribe(token);
}
#[test]
fn chunks_are_bounded() {
let relay = relay();
let (token, mut rx) = relay.subscribe();
relay.pump(&input( MAX_CHUNK * 2));
let chunks = drain(&mut rx);
assert!(
chunks.iter().all(|chunk| chunk.samples.len() <= MAX_CHUNK),
"单块不应超过 {MAX_CHUNK}"
);
relay.unsubscribe(token);
}
#[test]
fn slow_subscriber_skips_and_marks_gap() {
let relay = relay();
let (token, mut rx) = relay.subscribe();
for _ in 0..SUBSCRIBER_QUEUE_CHUNKS.saturating_add(3) {
relay.pump(&input( MAX_CHUNK));
}
let queued = drain(&mut rx);
assert_eq!(queued.len(), SUBSCRIBER_QUEUE_CHUNKS);
relay.pump(&input( MAX_CHUNK));
let resumed = drain(&mut rx);
assert!(
resumed.first().is_some_and(|chunk| chunk.gap),
"恢复后首块应标缺口"
);
relay.unsubscribe(token);
}
#[test]
fn track_change_bumps_generation() {
let relay = relay();
let (token, mut rx) = relay.subscribe();
relay.pump(&input( 100));
let _ = drain(&mut rx);
let before = relay.generation();
let mut next = input( 100);
next.track_token = 2;
relay.pump(&next);
assert_eq!(relay.generation(), before.saturating_add(1));
let chunks = drain(&mut rx);
assert!(
chunks
.first()
.is_some_and(|chunk| chunk.generation == before.saturating_add(1) && chunk.gap),
"换代后的首块应带新代次与缺口"
);
relay.unsubscribe(token);
}
#[test]
fn sample_rate_change_bumps_generation() {
let relay = relay();
relay.pump(&input( 10));
let baseline = relay.generation();
let mut same = input( 10);
same.sample_rate = 44_100;
relay.pump(&same);
assert_eq!(relay.generation(), baseline, "相同采样率不换代");
let mut changed = input( 10);
changed.sample_rate = 48_000;
relay.pump(&changed);
assert_eq!(relay.generation(), baseline.saturating_add(1));
}
#[test]
fn upstream_drop_marks_gap() {
let relay = relay();
let (token, mut rx) = relay.subscribe();
relay.pump(&input( 100));
let _ = drain(&mut rx);
let mut lossy = input( 100);
lossy.pushed_total = 100 + 100 + 50;
relay.pump(&lossy);
let chunks = drain(&mut rx);
assert!(
chunks.first().is_some_and(|chunk| chunk.gap),
"上游丢样本后应标缺口"
);
relay.unsubscribe(token);
}
#[test]
fn position_jump_bumps_generation() {
let relay = relay();
relay.pump(&input( 44_100));
let baseline = relay.generation();
let mut jumped = input( 100);
jumped.position_ms = 60_000;
relay.pump(&jumped);
assert_eq!(relay.generation(), baseline.saturating_add(1));
}
#[test]
fn window_is_bounded() {
let relay = relay();
for _ in 0..((MAX_HISTORY / MAX_CHUNK).saturating_add(2)) {
relay.pump(&input( MAX_CHUNK));
}
let (token, mut rx) = relay.subscribe();
relay.pump(&input( 10));
let chunks = drain(&mut rx);
let total: usize = chunks.iter().map(|chunk| chunk.samples.len()).sum();
assert!(total <= MAX_HISTORY.saturating_add(10), "窗口应有界");
let first = chunks.first();
assert!(first.is_some_and(|chunk| chunk.gap), "首块应标缺口");
assert!(
first.is_some_and(|chunk| chunk.position > 0),
"迟到订阅者不应从 0 开始"
);
relay.unsubscribe(token);
}
}