media_pp/elements/source/test/
audio.rs1use std::{
2 f64::consts::TAU,
3 sync::Arc,
4 thread,
5 time::{Duration, Instant},
6};
7
8use crate::pp_log::{PpLog, pp_info};
9use ffmpeg_next as ffmpeg;
10use thiserror::Error as ThisError;
11
12use crate::{
13 buffer::MediaBuffer,
14 bus::{Bus, BusEvent},
15 contract::{MediaKind, MemoryDomain, OutputContract, PortContract},
16 control::{ControlReceiver, drain_control},
17 element::{Element, ElementType, Source, SourceElement, element_pp_log},
18 error::Result,
19 pad::SrcPad,
20 schedule::ActiveTimeline,
21};
22
23const TICK_INTERVAL: Duration = Duration::from_millis(20);
28
29#[derive(Debug, ThisError)]
32pub enum TestAudioSourceError {
33 #[error("TestAudioSource doesn't support seeking a generated stream")]
35 SeekUnsupported,
36}
37
38#[derive(Debug, Clone, Copy)]
40pub struct TestAudioOptions {
41 pub sample_rate: u32,
43 pub channels: u16,
45 pub frequency: f64,
49}
50
51impl Default for TestAudioOptions {
52 fn default() -> Self {
53 Self {
54 sample_rate: 48000,
55 channels: 2,
56 frequency: 440.0,
57 }
58 }
59}
60
61pub struct TestAudioSource {
83 pp_log: PpLog,
84 name: Arc<str>,
85 pad: SrcPad,
86 sample_rate: u32,
87 channels: u16,
88 format: ffmpeg::format::Sample,
89 channel_layout: ffmpeg::ChannelLayout,
90 frequency: f64,
91 samples_emitted: i64,
98}
99
100unsafe impl Send for TestAudioSource {}
104
105impl TestAudioSource {
106 pub fn new(name: impl Into<String>, options: TestAudioOptions) -> Self {
108 let name: Arc<str> = name.into().into();
109 let pp_log = element_pp_log(ElementType::TestAudioSource, &name, None);
110 pp_info!(
111 pp_log: &pp_log,
112 "created: {}Hz, {} channel(s), {}Hz tone",
113 options.sample_rate,
114 options.channels,
115 options.frequency
116 );
117 let pad = SrcPad::with_contract(
118 format!("{name}_src"),
119 OutputContract::Fixed(PortContract::frame(
120 MediaKind::AudioFrame,
121 MemoryDomain::System,
122 )),
123 );
124 Self {
125 name,
126 pp_log,
127 pad,
128 sample_rate: options.sample_rate,
129 channels: options.channels,
130 format: ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
131 channel_layout: ffmpeg::ChannelLayout::default(options.channels as i32),
132 frequency: options.frequency,
133 samples_emitted: 0,
134 }
135 }
136
137 pub fn time_base(&self) -> ffmpeg::Rational {
139 ffmpeg::Rational::new(1, self.sample_rate as i32)
140 }
141
142 fn generate_frame(&mut self, needed: usize) -> ffmpeg::frame::Audio {
146 let channels = self.channels as usize;
147 let mut interleaved = vec![0f32; needed * channels];
148 for (index, chunk) in interleaved.chunks_mut(channels).enumerate() {
149 let t = (self.samples_emitted + index as i64) as f64 / self.sample_rate as f64;
150 let sample = (t * self.frequency * TAU).sin() as f32;
151 chunk.fill(sample);
152 }
153
154 let mut frame = ffmpeg::frame::Audio::new(self.format, needed, self.channel_layout);
155 frame.set_rate(self.sample_rate);
156 let bytes = unsafe {
160 std::slice::from_raw_parts(
161 interleaved.as_ptr() as *const u8,
162 std::mem::size_of_val(&*interleaved),
163 )
164 };
165 frame.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
170 frame.set_pts(Some(self.samples_emitted));
171 self.samples_emitted += needed as i64;
172 frame
173 }
174}
175
176impl Element for TestAudioSource {
177 fn name(&self) -> Arc<str> {
178 self.name.clone()
179 }
180
181 fn element_type(&self) -> ElementType {
182 ElementType::TestAudioSource
183 }
184
185 fn pp_log(&self) -> &crate::pp_log::PpLog {
186 &self.pp_log
187 }
188
189 fn pp_log_mut(&mut self) -> &mut crate::pp_log::PpLog {
190 &mut self.pp_log
191 }
192}
193
194impl Source for TestAudioSource {
195 fn src_pads(&mut self) -> &mut [SrcPad] {
196 std::slice::from_mut(&mut self.pad)
197 }
198}
199
200impl SourceElement for TestAudioSource {
201 fn is_live(&self) -> bool {
202 true
203 }
204
205 fn is_seekable(&self) -> bool {
206 false
207 }
208
209 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
210 pp_info!(self, "started");
211 let mut timeline = ActiveTimeline::new(Instant::now());
212 loop {
213 let outcome = drain_control(control, self, bus)?;
214 if outcome.stopped {
215 pp_info!(self, "stopped");
216 return Ok(());
217 }
218 timeline.account_pause(outcome.paused_for);
219 thread::sleep(TICK_INTERVAL);
220
221 let expected =
222 (timeline.elapsed(Instant::now()).as_secs_f64() * self.sample_rate as f64) as i64;
223 let needed = (expected - self.samples_emitted).max(0) as usize;
224 if needed == 0 {
225 continue;
226 }
227 let frame = self.generate_frame(needed);
228 if let Err(error) = self.pad.push(MediaBuffer::Audio(Arc::new(frame))) {
232 bus.post(
233 &self.pp_log,
234 BusEvent::Error {
235 element_type: ElementType::TestAudioSource,
236 name: self.name.clone(),
237 error,
238 },
239 );
240 }
241 }
242 }
243
244 fn seek(&mut self, _target: Duration) -> Result<Duration> {
245 Err(TestAudioSourceError::SeekUnsupported.into())
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use std::sync::Mutex;
252
253 use crate::pp_log::PpLog;
254
255 use super::*;
256 use crate::{control::ControlMsg, element::Sink, pipeline::Pipeline};
257
258 struct RecordingSink {
261 pp_log: PpLog,
262 #[allow(clippy::type_complexity)]
263 seen: Arc<Mutex<Vec<(ffmpeg::format::Sample, u32, u16, Option<i64>, f32)>>>,
264 }
265
266 impl Element for RecordingSink {
267 fn name(&self) -> Arc<str> {
268 "recorder".into()
269 }
270 fn element_type(&self) -> ElementType {
271 ElementType::Other
272 }
273 fn pp_log(&self) -> &PpLog {
274 &self.pp_log
275 }
276 fn pp_log_mut(&mut self) -> &mut PpLog {
277 &mut self.pp_log
278 }
279 }
280
281 impl Sink for RecordingSink {
282 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
283 if let MediaBuffer::Audio(frame) = buf
284 && frame.samples() > 0
285 {
286 self.seen.lock().unwrap().push((
287 frame.format(),
288 frame.rate(),
289 frame.channel_layout().channels() as u16,
290 frame.pts(),
291 frame.plane::<f32>(0)[0],
292 ));
293 }
294 Ok(())
295 }
296 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
297 Ok(())
298 }
299 }
300
301 #[test]
302 fn generates_f32_frames_with_increasing_pts_and_a_bounded_tone() {
303 let seen = Arc::new(Mutex::new(Vec::new()));
304 let sink = RecordingSink {
305 seen: seen.clone(),
306 pp_log: element_pp_log(ElementType::Other, "recorder", None),
307 };
308 let source = TestAudioSource::new(
309 "test-audio",
310 TestAudioOptions {
311 sample_rate: 48000,
312 channels: 2,
313 frequency: 440.0,
314 },
315 );
316
317 let pipeline = Pipeline::new("test", source, |source, ctx| {
318 let branch = ctx.branch().to(Box::new(sink))?;
319 ctx.attach(source, 0, branch)?;
320 Ok(())
321 })
322 .expect("test pipeline wiring must succeed");
323
324 pipeline.run().unwrap();
325 std::thread::sleep(Duration::from_millis(200));
327 pipeline.stop();
328 pipeline.bus().log_events();
329
330 let frames = seen.lock().unwrap();
331 assert!(!frames.is_empty(), "expected at least one generated frame");
332 for &(format, rate, channels, _, sample) in frames.iter() {
333 assert_eq!(
334 format,
335 ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed)
336 );
337 assert_eq!((rate, channels), (48000, 2));
338 assert!(
339 (-1.0..=1.0).contains(&sample),
340 "expected a bounded sine sample, got {sample}"
341 );
342 }
343 for window in frames.windows(2) {
344 assert!(
345 window[1].3 > window[0].3,
346 "expected pts to strictly increase frame over frame, got {:?} then {:?}",
347 window[0].3,
348 window[1].3
349 );
350 }
351 }
352
353 #[test]
354 fn seek_is_explicitly_unsupported() {
355 let mut source = TestAudioSource::new("test-audio", TestAudioOptions::default());
356 assert!(source.seek(Duration::from_secs(1)).is_err());
357 }
358
359 #[test]
369 fn resuming_after_a_pause_does_not_dump_a_burst_of_samples() {
370 let seen = Arc::new(Mutex::new(Vec::new()));
371 let sink = RecordingSink {
372 seen: seen.clone(),
373 pp_log: element_pp_log(ElementType::Other, "recorder", None),
374 };
375 let source = TestAudioSource::new(
376 "test-audio",
377 TestAudioOptions {
378 sample_rate: 48000,
379 channels: 2,
380 frequency: 440.0,
381 },
382 );
383
384 let pipeline = Pipeline::new("pause-resume-test", source, |source, ctx| {
385 let branch = ctx.branch().to(Box::new(sink))?;
386 ctx.attach(source, 0, branch)?;
387 Ok(())
388 })
389 .expect("test pipeline wiring must succeed");
390
391 pipeline.run().unwrap();
392 thread::sleep(Duration::from_millis(60));
393 pipeline.pause();
394 thread::sleep(Duration::from_millis(400));
395 pipeline.resume();
396 thread::sleep(Duration::from_millis(100));
397 pipeline.stop();
398 pipeline.bus().log_events();
399
400 let frames = seen.lock().unwrap();
401 let pts: Vec<i64> = frames.iter().filter_map(|&(_, _, _, pts, _)| pts).collect();
402 assert!(
403 pts.len() >= 2,
404 "expected multiple frames spanning the pause/resume, got {}",
405 pts.len()
406 );
407 for window in pts.windows(2) {
408 let gap = window[1] - window[0];
409 assert!(
414 gap < 12_000,
415 "expected steady per-tick sample counts across resume, not a single burst \
416 frame covering the whole pause: consecutive pts gap was {gap} samples \
417 ({:.0}ms) — full pts sequence: {pts:?}",
418 gap as f64 / 48.0
419 );
420 }
421 }
422}