use std::time::{Duration, Instant};
use flexaudio::core::types::{AudioChunk, ChunkFlags, OutputFormat, StreamConfig};
use flexaudio::{MockBackend, Stream};
fn collect_until(
stream: &mut Stream,
max_wait: Duration,
mut done: impl FnMut(&[AudioChunk]) -> bool,
) -> Vec<AudioChunk> {
let mut chunks = Vec::new();
let start = Instant::now();
loop {
while let Some(c) = stream.poll_chunk() {
chunks.push(c);
}
if done(&chunks) || start.elapsed() >= max_wait {
return chunks;
}
std::thread::sleep(Duration::from_millis(5));
}
}
const COLLECT_MAX_WAIT: Duration = Duration::from_secs(30);
const MIN_CHUNKS: usize = 10;
#[test]
fn mock_mono_44100_to_stereo_960_chunks() {
let backend = Box::new(MockBackend::new(44100, 1, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
stream.start().expect("start");
let chunks = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| c.len() >= MIN_CHUNKS);
stream.stop();
assert!(
chunks.len() >= MIN_CHUNKS,
"too few chunks arrived: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 960, "20 ms at 48 kHz is 960 frames");
assert_eq!(c.data.len(), 960 * 2, "stereo interleaved length is 960*2");
}
for w in chunks.windows(2) {
assert!(
w[1].seq > w[0].seq,
"seq is not monotonically increasing: {} -> {}",
w[0].seq,
w[1].seq
);
}
}
#[test]
fn mock_passthrough_48000_stereo() {
let backend = Box::new(MockBackend::new(48000, 2, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
stream.start().expect("start");
let chunks = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| c.len() >= MIN_CHUNKS);
stream.stop();
assert!(
chunks.len() >= MIN_CHUNKS,
"too few chunks arrived: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 960);
assert_eq!(c.data.len(), 1920);
}
}
#[test]
fn mock_output_16k_mono() {
let backend = Box::new(MockBackend::new(48_000, 2, 440.0));
let config = StreamConfig {
output: OutputFormat {
sample_rate: 16_000,
channels: 1,
},
..Default::default()
};
let mut stream = Stream::open(config, backend).expect("open");
stream.start().expect("start");
let chunks = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| c.len() >= MIN_CHUNKS);
stream.stop();
assert!(
chunks.len() >= MIN_CHUNKS,
"too few 16 kHz mono chunks arrived: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 320, "16 kHz 20 ms is 320 frames");
assert_eq!(c.data.len(), 320, "mono interleaved length is 320*1");
assert!(
c.peak > 0.0 && c.peak <= 1.5,
"peak is out of range: {}",
c.peak
);
assert!(
c.rms > 0.0 && c.rms <= 1.0,
"rms is out of range: {}",
c.rms
);
assert!(
c.peak >= c.rms,
"peak should be >= rms: peak={} rms={}",
c.peak,
c.rms
);
}
}
#[test]
fn mock_output_16k_stereo() {
let backend = Box::new(MockBackend::new(48_000, 2, 440.0));
let config = StreamConfig {
output: OutputFormat {
sample_rate: 16_000,
channels: 2,
},
..Default::default()
};
let mut stream = Stream::open(config, backend).expect("open");
stream.start().expect("start");
let chunks = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| c.len() >= MIN_CHUNKS);
stream.stop();
assert!(
chunks.len() >= MIN_CHUNKS,
"too few 16 kHz stereo chunks arrived: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 320, "16 kHz 20 ms is 320 frames");
assert_eq!(c.data.len(), 640, "stereo interleaved length is 320*2");
assert!(
c.peak > 0.0 && c.peak <= 1.5,
"peak is out of range: {}",
c.peak
);
assert!(
c.rms > 0.0 && c.rms <= 1.0,
"rms is out of range: {}",
c.rms
);
}
}
#[test]
fn mock_default_output_regression_with_peak_rms() {
let backend = Box::new(MockBackend::new(48_000, 2, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
stream.start().expect("start");
let chunks = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| c.len() >= MIN_CHUNKS);
stream.stop();
assert!(
chunks.len() >= MIN_CHUNKS,
"too few chunks arrived: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 960);
assert_eq!(c.data.len(), 1920);
assert!(c.peak > 0.0 && c.peak <= 1.5, "peak: {}", c.peak);
assert!(c.rms > 0.0 && c.rms <= 1.0, "rms: {}", c.rms);
}
}
#[test]
fn devices_enumeration_never_panics_and_is_consistent() {
use flexaudio::core::types::SourceKind;
let devices = flexaudio::devices().expect("devices() is designed not to return Err");
for d in &devices {
assert!(!d.id.is_empty(), "id (stable key) is nonempty");
assert!(d.sample_rate > 0, "sample_rate is positive");
assert!(d.channels > 0, "channels is positive");
match d.source_kind {
SourceKind::Mic => assert!(!d.is_loopback, "Mic is not loopback"),
SourceKind::SystemLoopback => assert!(d.is_loopback, "SystemLoopback is loopback"),
other => panic!("unexpected source_kind from devices(): {other:?}"),
}
}
}
#[test]
fn open_start_stop_is_clean() {
let backend = Box::new(MockBackend::new(48000, 2, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
stream.start().expect("start");
std::thread::sleep(Duration::from_millis(50));
stream.stop();
}
#[test]
fn switch_backend_keeps_seq_continuous_and_flags_discontinuity() {
fn is_switch_marker(c: &AudioChunk) -> bool {
c.flags.contains(ChunkFlags::DISCONTINUITY) && !c.flags.contains(ChunkFlags::RECOVERED)
}
let config = StreamConfig {
ring_capacity_chunks: 4096,
..Default::default()
};
let backend = Box::new(MockBackend::new(44_100, 1, 440.0));
let mut stream = Stream::open(config, backend).expect("open");
stream.start().expect("start");
assert_eq!(stream.native_format(), (44_100, 1));
let before = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| c.len() >= MIN_CHUNKS);
assert!(
before.len() >= MIN_CHUNKS,
"too few chunks arrived before the swap: {}",
before.len()
);
let before_count = before.len();
let new_backend = Box::new(MockBackend::new(48_000, 2, 220.0));
stream
.switch_backend(new_backend)
.expect("switch_backend should succeed");
assert_eq!(stream.native_format(), (48_000, 2));
let mut after = collect_until(&mut stream, COLLECT_MAX_WAIT, |c| {
c.iter()
.position(|chunk| chunk.flags.contains(ChunkFlags::DISCONTINUITY))
.is_some_and(|pos| c.len() - (pos + 1) >= MIN_CHUNKS)
});
stream.stop();
while let Some(c) = stream.poll_chunk() {
after.push(c);
}
assert!(!after.is_empty(), "no chunks arrived after the swap");
let mut all: Vec<AudioChunk> = Vec::with_capacity(before.len() + after.len());
all.extend(before);
all.extend(after);
for (i, c) in all.iter().enumerate() {
assert_eq!(
c.seq, i as u64,
"seq is not contiguous: index {i} has seq {} (gap)",
c.seq
);
}
let recovery_flags = ChunkFlags::RECOVERED | ChunkFlags::DISCONTINUITY;
for c in &all {
assert!(
c.flags.is_empty() || c.flags == ChunkFlags::DISCONTINUITY || c.flags == recovery_flags,
"flag outside allowed set (swap/recovery flagging is broken): seq={} flags={:?}",
c.seq,
c.flags
);
}
let marker_positions: Vec<usize> = all
.iter()
.enumerate()
.filter(|(_, c)| is_switch_marker(c))
.map(|(i, _)| i)
.collect();
assert!(
marker_positions.len() <= 1,
"swap DISCONTINUITY was set multiple times: positions={marker_positions:?}"
);
if let Some(&idx) = marker_positions.first() {
assert!(
idx >= before_count,
"DISCONTINUITY was set before the swap: idx={idx} < before_count={before_count}"
);
}
assert!(
all[before_count..]
.iter()
.any(|c| c.flags.contains(ChunkFlags::DISCONTINUITY)),
"no DISCONTINUITY at or after the swap boundary (swap did not report discontinuity)"
);
for c in &all {
assert_eq!(c.frames, 960, "frames is not 960: seq={}", c.seq);
assert_eq!(
c.data.len(),
1920,
"data.len is not 1920 (960*2): seq={}",
c.seq
);
}
const MAX_PTS_BACKWARD_NS: i64 = 1_200_000_000;
for w in all.windows(2) {
assert!(
w[1].pts_ns >= w[0].pts_ns - MAX_PTS_BACKWARD_NS,
"pts_ns regressed beyond the re-anchor bound: {} -> {}",
w[0].pts_ns,
w[1].pts_ns
);
}
}
#[test]
fn switch_source_rejects_output_change() {
use flexaudio::core::types::{Error, SourceKind};
let backend = Box::new(MockBackend::new(48_000, 2, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
stream.start().expect("start");
let new_config = StreamConfig {
kind: SourceKind::Mic,
output: OutputFormat {
sample_rate: 16_000,
channels: 1,
},
..Default::default()
};
let err = stream
.switch_source(new_config)
.expect_err("output change should be rejected");
assert!(
matches!(err, Error::InvalidArg(_)),
"expected InvalidArg: {err:?}"
);
stream.stop();
}
#[test]
fn switch_backend_on_unstarted_is_invalid_state() {
use flexaudio::core::types::Error;
let backend = Box::new(MockBackend::new(48_000, 2, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
let new_backend = Box::new(MockBackend::new(48_000, 2, 220.0));
let err = stream
.switch_backend(new_backend)
.expect_err("before start, expected InvalidState");
assert!(
matches!(err, Error::InvalidState(_)),
"expected InvalidState: {err:?}"
);
stream.stop();
}
#[test]
fn switch_source_on_unstarted_is_invalid_state() {
use flexaudio::core::types::{Error, SourceKind};
let backend = Box::new(MockBackend::new(48_000, 2, 440.0));
let mut stream = Stream::open(StreamConfig::default(), backend).expect("open");
let new_config = StreamConfig {
kind: SourceKind::Mic,
..Default::default()
};
let err = stream
.switch_source(new_config)
.expect_err("before start, expected InvalidState");
assert!(
matches!(err, Error::InvalidState(_)),
"expected InvalidState: {err:?}"
);
stream.stop();
}