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,
"チャンクが相応に来ていない: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 960, "20ms@48k = 960 frame でない");
assert_eq!(c.data.len(), 960 * 2, "stereo interleaved (960*2) でない");
}
for w in chunks.windows(2) {
assert!(
w[1].seq > w[0].seq,
"seq が単調増加していない: {} -> {}",
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,
"チャンクが相応に来ていない: {}",
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,
"16k/mono チャンクが相応に来ていない: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 320, "16k 20ms = 320 frame でない");
assert_eq!(c.data.len(), 320, "mono interleaved (320*1) でない");
assert!(
c.peak > 0.0 && c.peak <= 1.5,
"peak が妥当でない: {}",
c.peak
);
assert!(c.rms > 0.0 && c.rms <= 1.0, "rms が妥当でない: {}", c.rms);
assert!(
c.peak >= c.rms,
"peak >= 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,
"16k/stereo チャンクが相応に来ていない: {}",
chunks.len()
);
for c in &chunks {
assert_eq!(c.frames, 320, "16k 20ms = 320 frame でない");
assert_eq!(c.data.len(), 640, "stereo interleaved (320*2) でない");
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 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,
"チャンクが相応に来ていない: {}",
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() は Err を返さない設計");
for d in &devices {
assert!(!d.id.is_empty(), "id(安定キー)は空でない");
assert!(d.sample_rate > 0, "sample_rate は正");
assert!(d.channels > 0, "channels は正");
match d.source_kind {
SourceKind::Mic => assert!(!d.is_loopback, "Mic はループバックでない"),
SourceKind::SystemLoopback => assert!(d.is_loopback, "SystemLoopback はループバック"),
other => panic!("devices() が返さないはずの source_kind: {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,
"切替前にチャンクが相応に来ていない: {}",
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(), "切替後にチャンクが来ていない");
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 が連続していない: index {i} に 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,
"許容集合外のフラグ(切替/復帰のフラグ付けが壊れている): 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,
"切替の DISCONTINUITY が複数回立っている: 位置={marker_positions:?}"
);
if let Some(&idx) = marker_positions.first() {
assert!(
idx >= before_count,
"DISCONTINUITY が切替前に立っている: idx={idx} < before_count={before_count}"
);
}
assert!(
all[before_count..]
.iter()
.any(|c| c.flags.contains(ChunkFlags::DISCONTINUITY)),
"切替境界以降に DISCONTINUITY が 1 つも無い(切替が不連続を通知していない)"
);
for c in &all {
assert_eq!(c.frames, 960, "frames が 960 でない: seq={}", c.seq);
assert_eq!(
c.data.len(),
1920,
"data.len が 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 が再アンカーの上界を超えて後退した: {} -> {}",
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 変更は弾かれるべき");
assert!(
matches!(err, Error::InvalidArg(_)),
"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("未 start では InvalidState のはず");
assert!(
matches!(err, Error::InvalidState(_)),
"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("未 start では InvalidState のはず");
assert!(
matches!(err, Error::InvalidState(_)),
"InvalidState であるべき: {err:?}"
);
stream.stop();
}