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