1use crate::audio::{AudioChunk, AudioFormat};
24use crate::clock::MediaTime;
25
26#[derive(Debug, Clone)]
29pub struct SineWaveSource {
30 format: AudioFormat,
31 frequency: f32,
33 amplitude: f32,
35 next_frame: u64,
37}
38
39impl SineWaveSource {
40 #[must_use]
43 pub fn new(format: AudioFormat, frequency_hz: f32, amplitude: f32) -> Self {
44 Self {
45 format,
46 frequency: frequency_hz.max(0.0),
47 amplitude: amplitude.clamp(0.0, 1.0),
48 next_frame: 0,
49 }
50 }
51
52 #[must_use]
54 pub fn format(&self) -> AudioFormat {
55 self.format
56 }
57
58 #[must_use]
60 pub fn frames_emitted(&self) -> u64 {
61 self.next_frame
62 }
63
64 pub fn next_chunk(&mut self, frames: u64) -> AudioChunk {
68 let pts = MediaTime::from_sample(self.next_frame, self.format.sample_rate);
69 let total_samples = usize::try_from(frames).expect("frames fits in usize")
70 * usize::from(self.format.channels);
71 let mut samples = Vec::with_capacity(total_samples);
72 for f in 0..frames {
73 let frame_idx = self.next_frame + f;
74 let t = frame_idx_to_seconds(frame_idx, self.format.sample_rate);
75 let v = self.amplitude * (std::f32::consts::TAU * self.frequency * t).sin();
76 for _ in 0..self.format.channels {
77 samples.push(v);
78 }
79 }
80 self.next_frame = self.next_frame.saturating_add(frames);
81 AudioChunk::new(self.format, samples, pts).expect("sine source produces valid chunk")
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct SilenceSource {
88 format: AudioFormat,
89 next_frame: u64,
90}
91
92impl SilenceSource {
93 #[must_use]
95 pub fn new(format: AudioFormat) -> Self {
96 Self {
97 format,
98 next_frame: 0,
99 }
100 }
101
102 pub fn next_chunk(&mut self, frames: u64) -> AudioChunk {
104 let pts = MediaTime::from_sample(self.next_frame, self.format.sample_rate);
105 let total_samples = usize::try_from(frames).expect("frames fits in usize")
106 * usize::from(self.format.channels);
107 let samples = vec![0.0_f32; total_samples];
108 self.next_frame = self.next_frame.saturating_add(frames);
109 AudioChunk::new(self.format, samples, pts).expect("silence source produces valid chunk")
110 }
111}
112
113#[derive(Debug, Clone)]
117pub struct StepPulseSource {
118 format: AudioFormat,
119 spike_frame_index: u64,
120 next_frame: u64,
121}
122
123impl StepPulseSource {
124 #[must_use]
126 pub fn new(format: AudioFormat, spike_frame_index: u64) -> Self {
127 Self {
128 format,
129 spike_frame_index,
130 next_frame: 0,
131 }
132 }
133
134 pub fn next_chunk(&mut self, frames: u64) -> AudioChunk {
136 let pts = MediaTime::from_sample(self.next_frame, self.format.sample_rate);
137 let total_samples = usize::try_from(frames).expect("frames fits in usize")
138 * usize::from(self.format.channels);
139 let mut samples = vec![0.0_f32; total_samples];
140 if self.spike_frame_index >= self.next_frame
141 && self.spike_frame_index < self.next_frame + frames
142 {
143 let offset_frames =
144 usize::try_from(self.spike_frame_index - self.next_frame).expect("fits in usize");
145 let base = offset_frames * usize::from(self.format.channels);
146 for c in 0..usize::from(self.format.channels) {
147 samples[base + c] = 1.0;
148 }
149 }
150 self.next_frame = self.next_frame.saturating_add(frames);
151 AudioChunk::new(self.format, samples, pts).expect("pulse source produces valid chunk")
152 }
153}
154
155fn frame_idx_to_seconds(frame: u64, sample_rate: u32) -> f32 {
156 #[expect(
159 clippy::cast_precision_loss,
160 reason = "frame counts above 2^53 (≈ 195 days at 48 kHz) are not realistic"
161 )]
162 let frame_f = frame as f64;
163 let seconds = frame_f / f64::from(sample_rate);
164 #[expect(
165 clippy::cast_possible_truncation,
166 reason = "seconds in the realistic range fit f32"
167 )]
168 let s = seconds as f32;
169 s
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::audio::AudioFormat;
176
177 #[test]
178 fn sine_source_emits_expected_sample_count() {
179 let fmt = AudioFormat::mono_f32(48_000);
180 let mut src = SineWaveSource::new(fmt, 440.0, 0.5);
181 let chunk = src.next_chunk(480); assert_eq!(chunk.samples().len(), 480);
183 assert_eq!(chunk.frame_count(), 480);
184 assert_eq!(src.frames_emitted(), 480);
185 }
186
187 #[test]
188 fn sine_source_stereo_interleaves() {
189 let fmt = AudioFormat::stereo_f32(48_000);
190 let mut src = SineWaveSource::new(fmt, 440.0, 1.0);
191 let chunk = src.next_chunk(4);
192 assert_eq!(chunk.samples().len(), 8);
195 for pair in chunk.samples().chunks_exact(2) {
196 assert!((pair[0] - pair[1]).abs() < f32::EPSILON);
197 }
198 }
199
200 #[test]
201 fn sine_source_peak_approximates_amplitude() {
202 let fmt = AudioFormat::mono_f32(48_000);
205 let mut src = SineWaveSource::new(fmt, 440.0, 0.5);
206 let chunk = src.next_chunk(480);
207 let peak = chunk.peak();
208 assert!((peak - 0.5).abs() < 0.01, "expected peak ~ 0.5, got {peak}");
209 }
210
211 #[test]
212 fn sine_source_rms_approximates_amp_over_sqrt2() {
213 let fmt = AudioFormat::mono_f32(48_000);
215 let mut src = SineWaveSource::new(fmt, 1_000.0, 1.0);
216 let chunk = src.next_chunk(4_800);
218 let rms = chunk.rms();
219 let expected = 1.0 / 2.0_f32.sqrt();
220 assert!(
221 (rms - expected).abs() < 0.01,
222 "expected rms ~ {expected}, got {rms}"
223 );
224 }
225
226 #[test]
227 fn sine_source_pts_advances_between_calls() {
228 let fmt = AudioFormat::mono_f32(48_000);
229 let mut src = SineWaveSource::new(fmt, 440.0, 0.5);
230 let c0 = src.next_chunk(48_000); let c1 = src.next_chunk(48_000); assert!((c0.pts().as_seconds() - 0.0).abs() < 1e-12);
233 assert!((c1.pts().as_seconds() - 1.0).abs() < 1e-12);
234 }
235
236 #[test]
237 fn silence_source_emits_zeros() {
238 let fmt = AudioFormat::mono_f32(48_000);
239 let mut src = SilenceSource::new(fmt);
240 let chunk = src.next_chunk(100);
241 assert_eq!(chunk.samples().len(), 100);
242 for s in chunk.samples() {
243 assert!(s.abs() < f32::EPSILON);
244 }
245 assert!(chunk.peak() < f32::EPSILON);
246 assert!(chunk.rms() < f32::EPSILON);
247 }
248
249 #[test]
250 fn pulse_source_emits_spike_at_expected_frame() {
251 let fmt = AudioFormat::mono_f32(48_000);
252 let mut src = StepPulseSource::new(fmt, 7);
253 let chunk = src.next_chunk(16);
254 for (i, &s) in chunk.samples().iter().enumerate() {
256 if i == 7 {
257 assert!((s - 1.0).abs() < f32::EPSILON);
258 } else {
259 assert!(s.abs() < f32::EPSILON);
260 }
261 }
262 assert!((chunk.peak() - 1.0).abs() < f32::EPSILON);
263 }
264
265 #[test]
266 fn pulse_source_handles_spike_outside_window() {
267 let fmt = AudioFormat::mono_f32(48_000);
269 let mut src = StepPulseSource::new(fmt, 100);
270 let chunk = src.next_chunk(16);
271 assert!(chunk.peak() < f32::EPSILON);
273 let _ = src.next_chunk(80);
275 let chunk = src.next_chunk(16);
276 assert!((chunk.peak() - 1.0).abs() < f32::EPSILON);
279 assert!((chunk.samples()[4] - 1.0).abs() < f32::EPSILON);
280 }
281
282 #[test]
283 fn pulse_source_stereo_spike_fills_both_channels() {
284 let fmt = AudioFormat::stereo_f32(48_000);
285 let mut src = StepPulseSource::new(fmt, 2);
286 let chunk = src.next_chunk(4);
287 for (i, &s) in chunk.samples().iter().enumerate() {
289 if i == 4 || i == 5 {
290 assert!((s - 1.0).abs() < f32::EPSILON);
291 } else {
292 assert!(s.abs() < f32::EPSILON);
293 }
294 }
295 }
296
297 #[test]
298 fn sources_have_no_gstreamer_or_device_dependency() {
299 let fmt = AudioFormat::mono_f32(48_000);
303 let _ = SineWaveSource::new(fmt, 440.0, 0.5).next_chunk(10);
304 let _ = SilenceSource::new(fmt).next_chunk(10);
305 let _ = StepPulseSource::new(fmt, 5).next_chunk(10);
306 }
307
308 #[test]
309 fn types_are_send_and_sync() {
310 fn assert_send_sync<T: Send + Sync>() {}
311 assert_send_sync::<SineWaveSource>();
312 assert_send_sync::<SilenceSource>();
313 assert_send_sync::<StepPulseSource>();
314 }
315}