Skip to main content

active_call/media/track/
file.rs

1use crate::event::{EventSender, SessionEvent};
2use crate::media::processor::ProcessorChain;
3use crate::media::{AudioFrame, PcmBuf, Samples, TrackId};
4use crate::media::track::{Track, TrackConfig, TrackPacketSender};
5use anyhow::{Result, anyhow};
6use async_trait::async_trait;
7use audio_codec::BoxedResampler;
8use hound::WavReader;
9use std::cmp::min;
10use std::fs::File;
11use std::io::BufReader;
12use std::sync::{
13    Arc,
14    atomic::{AtomicBool, Ordering},
15};
16use std::time::Instant;
17use tokio::select;
18use tokio::time::Duration;
19use tokio_util::sync::CancellationToken;
20use tracing::{debug, info, warn};
21
22trait AudioReader: Send {
23    fn fill_buffer(&mut self) -> Result<usize>;
24
25    fn read_chunk(&mut self, packet_duration_ms: u32) -> Result<Option<(PcmBuf, u32)>> {
26        let max_chunk_size = self.sample_rate() as usize * packet_duration_ms as usize / 1000;
27
28        if self.buffer_size() == 0 || self.position() >= self.buffer_size() {
29            let samples_read = self.fill_buffer()?;
30            if samples_read == 0 {
31                return Ok(None);
32            }
33            self.set_position(0);
34        }
35
36        let remaining = self.buffer_size() - self.position();
37        if remaining == 0 {
38            return Ok(None);
39        }
40
41        let chunk_size = min(max_chunk_size, remaining);
42        let end_pos = self.position() + chunk_size;
43
44        assert!(
45            end_pos <= self.buffer_size(),
46            "Buffer overrun: pos={}, end={}, size={}",
47            self.position(),
48            end_pos,
49            self.buffer_size()
50        );
51
52        let chunk = self.extract_chunk(self.position(), end_pos);
53        self.set_position(end_pos);
54
55        let final_chunk =
56            if self.sample_rate() != self.target_sample_rate() && self.sample_rate() > 0 {
57                self.resample_chunk(&chunk)
58            } else {
59                chunk
60            };
61
62        Ok(Some((final_chunk, self.target_sample_rate())))
63    }
64
65    fn buffer_size(&self) -> usize;
66    fn position(&self) -> usize;
67    fn set_position(&mut self, pos: usize);
68    fn sample_rate(&self) -> u32;
69    fn target_sample_rate(&self) -> u32;
70    fn channels(&self) -> u16;
71    fn extract_chunk(&self, start: usize, end: usize) -> Vec<i16>;
72    fn resample_chunk(&mut self, chunk: &[i16]) -> Vec<i16>;
73}
74
75struct DecodedAudioReader {
76    buffer: Vec<i16>,
77    sample_rate: u32,
78    position: usize,
79    target_sample_rate: u32,
80    resampler: Option<BoxedResampler>,
81}
82
83impl DecodedAudioReader {
84    #[cfg(test)]
85    fn from_file(
86        file: File,
87        extension: &str,
88        mime_type: Option<&str>,
89        target_sample_rate: u32,
90    ) -> Result<Self> {
91        let all_samples =
92            crate::media::loader::decode_audio(file, extension, mime_type, target_sample_rate)?;
93        Ok(Self {
94            buffer: all_samples,
95            sample_rate: target_sample_rate,
96            position: 0,
97            target_sample_rate,
98            resampler: None,
99        })
100    }
101
102    /// Build a reader from already-decoded PCM samples. The samples are assumed
103    /// to already be at `sample_rate`, so no further resampling happens when it
104    /// matches `target_sample_rate`.
105    fn from_samples(buffer: Vec<i16>, sample_rate: u32, target_sample_rate: u32) -> Self {
106        Self {
107            buffer,
108            sample_rate,
109            position: 0,
110            target_sample_rate,
111            resampler: None,
112        }
113    }
114}
115
116impl AudioReader for DecodedAudioReader {
117    fn fill_buffer(&mut self) -> Result<usize> {
118        if self.position >= self.buffer.len() {
119            return Ok(0);
120        }
121        Ok(self.buffer.len() - self.position)
122    }
123
124    fn buffer_size(&self) -> usize {
125        self.buffer.len()
126    }
127
128    fn position(&self) -> usize {
129        self.position
130    }
131
132    fn set_position(&mut self, pos: usize) {
133        self.position = pos;
134    }
135
136    fn sample_rate(&self) -> u32 {
137        self.sample_rate
138    }
139
140    fn target_sample_rate(&self) -> u32 {
141        self.target_sample_rate
142    }
143
144    fn channels(&self) -> u16 {
145        1
146    }
147
148    fn extract_chunk(&self, start: usize, end: usize) -> Vec<i16> {
149        self.buffer[start..end].to_vec()
150    }
151
152    fn resample_chunk(&mut self, chunk: &[i16]) -> Vec<i16> {
153        if self.sample_rate == 0 || self.sample_rate == self.target_sample_rate {
154            return chunk.to_vec();
155        }
156
157        if let Some(resampler) = &mut self.resampler {
158            resampler.resample(chunk)
159        } else {
160            let mut new_resampler = BoxedResampler::new(
161                self.sample_rate as usize,
162                self.target_sample_rate as usize,
163            )
164            .expect("invalid sample rate");
165            let result = new_resampler.resample(chunk);
166            self.resampler = Some(new_resampler);
167            result
168        }
169    }
170}
171
172// Unified function to process any audio reader and stream audio
173async fn process_audio_reader(
174    mut processor_chain: ProcessorChain,
175    mut audio_reader: Box<dyn AudioReader>,
176    track_id: &str,
177    packet_duration_ms: u32,
178    target_sample_rate: u32,
179    token: CancellationToken,
180    paused: Arc<AtomicBool>,
181    packet_sender: TrackPacketSender,
182) -> Result<()> {
183    info!(
184        "streaming audio with target_sample_rate: {}, packet_duration: {}ms",
185        target_sample_rate, packet_duration_ms
186    );
187    let stream_loop = async move {
188        let start_time = Instant::now();
189        let mut ticker = tokio::time::interval(Duration::from_millis(packet_duration_ms as u64));
190        let channels = audio_reader.channels();
191        loop {
192            if paused.load(Ordering::Relaxed) {
193                ticker.tick().await;
194                continue;
195            }
196
197            let Some((chunk, chunk_sample_rate)) = audio_reader.read_chunk(packet_duration_ms)?
198            else {
199                break;
200            };
201
202            let mut packet = AudioFrame {
203                track_id: track_id.to_string(),
204                timestamp: crate::media::get_timestamp(),
205                samples: Samples::PCM { samples: chunk },
206                sample_rate: chunk_sample_rate,
207                channels,
208                ..Default::default()
209            };
210
211            match processor_chain.process_frame(&mut packet) {
212                Ok(_) => {}
213                Err(e) => {
214                    warn!("failed to process audio packet: {}", e);
215                }
216            }
217
218            if let Err(e) = packet_sender.send(packet) {
219                warn!("failed to send audio packet: {}", e);
220                break;
221            }
222
223            ticker.tick().await;
224        }
225
226        info!("stream loop finished in {:?}", start_time.elapsed());
227        Ok(()) as Result<()>
228    };
229
230    select! {
231        _ = token.cancelled() => {
232            info!("stream cancelled");
233            return Ok(());
234        }
235        result = stream_loop => {
236            info!("stream loop finished");
237            result
238        }
239    }
240}
241
242pub struct FileTrack {
243    track_id: TrackId,
244    play_id: Option<String>,
245    config: TrackConfig,
246    cancel_token: CancellationToken,
247    processor_chain: ProcessorChain,
248    path: Option<String>,
249    use_cache: bool,
250    ssrc: u32,
251    offset_ms: u32,
252    paused: Arc<AtomicBool>,
253}
254
255impl FileTrack {
256    pub fn new(id: TrackId) -> Self {
257        let config = TrackConfig::default();
258        Self {
259            track_id: id,
260            play_id: None,
261            processor_chain: ProcessorChain::new(config.samplerate),
262            config,
263            cancel_token: CancellationToken::new(),
264            path: None,
265            use_cache: true,
266            ssrc: 0,
267            offset_ms: 0,
268            paused: Arc::new(AtomicBool::new(false)),
269        }
270    }
271
272    pub fn with_play_id(mut self, play_id: Option<String>) -> Self {
273        self.play_id = play_id;
274        self
275    }
276
277    pub fn with_ssrc(mut self, ssrc: u32) -> Self {
278        self.ssrc = ssrc;
279        self
280    }
281    pub fn with_config(mut self, config: TrackConfig) -> Self {
282        self.config = config;
283        self
284    }
285
286    pub fn with_cancel_token(mut self, cancel_token: CancellationToken) -> Self {
287        self.cancel_token = cancel_token;
288        self
289    }
290
291    pub fn with_path(mut self, path: String) -> Self {
292        self.path = Some(path);
293        self
294    }
295
296    pub fn with_sample_rate(mut self, sample_rate: u32) -> Self {
297        self.config = self.config.with_sample_rate(sample_rate);
298        self
299    }
300
301    pub fn with_ptime(mut self, ptime: Duration) -> Self {
302        self.config = self.config.with_ptime(ptime);
303        self
304    }
305
306    pub fn with_cache_enabled(mut self, use_cache: bool) -> Self {
307        self.use_cache = use_cache;
308        self
309    }
310
311    pub fn with_offset_ms(mut self, offset_ms: u32) -> Self {
312        self.offset_ms = offset_ms;
313        self
314    }
315}
316
317#[async_trait]
318impl Track for FileTrack {
319    fn ssrc(&self) -> u32 {
320        self.ssrc
321    }
322    fn id(&self) -> &TrackId {
323        &self.track_id
324    }
325    fn config(&self) -> &TrackConfig {
326        &self.config
327    }
328    fn set_paused(&self, paused: bool) -> bool {
329        self.paused.store(paused, Ordering::Relaxed);
330        true
331    }
332    fn is_paused(&self) -> bool {
333        self.paused.load(Ordering::Relaxed)
334    }
335    fn processor_chain(&mut self) -> &mut ProcessorChain {
336        &mut self.processor_chain
337    }
338
339    async fn handshake(&mut self, _offer: String, _timeout: Option<Duration>) -> Result<String> {
340        Ok("".to_string())
341    }
342    async fn update_remote_description(&mut self, _answer: &String) -> Result<()> {
343        Ok(())
344    }
345
346    async fn start(
347        &mut self,
348        event_sender: EventSender,
349        packet_sender: TrackPacketSender,
350    ) -> Result<()> {
351        if self.path.is_none() {
352            return Err(anyhow::anyhow!("filetrack: No path provided for FileTrack"));
353        }
354        let path = self.path.clone().unwrap();
355        let id = self.track_id.clone();
356        let sample_rate = self.config.samplerate;
357        let use_cache = self.use_cache;
358        let packet_duration_ms = self.config.ptime.as_millis() as u32;
359        let processor_chain = self.processor_chain.clone();
360        let token = self.cancel_token.clone();
361        let start_time = crate::media::get_timestamp();
362        let ssrc = self.ssrc;
363        let offset_ms = self.offset_ms;
364        let paused = self.paused.clone();
365        // Spawn async task to handle file streaming
366        let play_id = self.play_id.clone();
367        crate::spawn(async move {
368            let res = async move {
369                // Load (and cache) the decoded PCM so we don't re-download or
370                // re-decode the original file on every play.
371                let load_result = crate::media::loader::load_audio_as_pcm_cached(
372                    &path,
373                    sample_rate,
374                    use_cache,
375                    offset_ms,
376                )
377                .await;
378                let samples = match load_result {
379                    Ok(samples) => samples,
380                    Err(e) => {
381                        warn!("filetrack: Error loading audio: {} {}", path, e);
382                        event_sender
383                            .send(SessionEvent::Error {
384                                track_id: id.clone(),
385                                timestamp: crate::media::get_timestamp(),
386                                sender: format!("filetrack: {}", path),
387                                error: e.to_string(),
388                                code: None,
389                            })
390                            .ok();
391                        event_sender
392                            .send(SessionEvent::TrackEnd {
393                                track_id: id,
394                                timestamp: crate::media::get_timestamp(),
395                                duration: crate::media::get_timestamp() - start_time,
396                                ssrc,
397                                play_id: play_id.clone(),
398                            })
399                            .ok();
400                        return Err(e);
401                    }
402                };
403
404                // Stream the decoded PCM (offset already applied during load)
405                let stream_result = stream_pcm_samples(
406                    processor_chain,
407                    samples,
408                    sample_rate,
409                    &id,
410                    packet_duration_ms,
411                    token,
412                    paused,
413                    packet_sender,
414                )
415                .await;
416
417                // Handle any streaming errors
418                if let Err(e) = stream_result {
419                    warn!("filetrack: Error streaming audio: {}, {}", path, e);
420                    event_sender
421                        .send(SessionEvent::Error {
422                            track_id: id.clone(),
423                            timestamp: crate::media::get_timestamp(),
424                            sender: format!("filetrack: {}", path),
425                            error: e.to_string(),
426                            code: None,
427                        })
428                        .ok();
429                }
430
431                // Send track end event
432                event_sender
433                    .send(SessionEvent::TrackEnd {
434                        track_id: id,
435                        timestamp: crate::media::get_timestamp(),
436                        duration: crate::media::get_timestamp() - start_time,
437                        ssrc,
438                        play_id,
439                    })
440                    .ok();
441                Ok::<(), anyhow::Error>(())
442            }
443            .await;
444            if let Err(e) = res {
445                debug!("filetrack: streaming task finished with error: {:?}", e);
446            }
447        });
448        Ok(())
449    }
450
451    async fn stop(&self) -> Result<()> {
452        // Cancel the file streaming task
453        self.cancel_token.cancel();
454        Ok(())
455    }
456
457    // Do nothing as we are not sending packets
458    async fn send_packet(&mut self, _packet: &AudioFrame) -> Result<()> {
459        Ok(())
460    }
461}
462
463// Helper function to stream already-decoded PCM samples
464async fn stream_pcm_samples(
465    processor_chain: ProcessorChain,
466    samples: Vec<i16>,
467    target_sample_rate: u32,
468    track_id: &str,
469    packet_duration_ms: u32,
470    token: CancellationToken,
471    paused: Arc<AtomicBool>,
472    packet_sender: TrackPacketSender,
473) -> Result<()> {
474    let reader =
475        DecodedAudioReader::from_samples(samples, target_sample_rate, target_sample_rate);
476    let audio_reader = Box::new(reader) as Box<dyn AudioReader>;
477    info!(
478        "filetrack: streaming {} decoded samples at {} Hz",
479        audio_reader.buffer_size(),
480        audio_reader.sample_rate(),
481    );
482    process_audio_reader(
483        processor_chain,
484        audio_reader,
485        track_id,
486        packet_duration_ms,
487        target_sample_rate,
488        token,
489        paused,
490        packet_sender,
491    )
492    .await
493}
494
495/// Read WAV file and return PCM samples and sample rate
496pub fn read_wav_file(path: &str) -> Result<(PcmBuf, u32)> {
497    let reader = BufReader::new(File::open(path)?);
498    let mut wav_reader = WavReader::new(reader)?;
499    let spec = wav_reader.spec();
500    let mut all_samples = Vec::new();
501
502    match spec.sample_format {
503        hound::SampleFormat::Int => match spec.bits_per_sample {
504            16 => {
505                for sample in wav_reader.samples::<i16>() {
506                    all_samples.push(sample.unwrap_or(0));
507                }
508            }
509            8 => {
510                for sample in wav_reader.samples::<i8>() {
511                    all_samples.push(sample.unwrap_or(0) as i16);
512                }
513            }
514            24 | 32 => {
515                for sample in wav_reader.samples::<i32>() {
516                    all_samples.push((sample.unwrap_or(0) >> 16) as i16);
517                }
518            }
519            _ => {
520                return Err(anyhow!(
521                    "Unsupported bits per sample: {}",
522                    spec.bits_per_sample
523                ));
524            }
525        },
526        hound::SampleFormat::Float => {
527            for sample in wav_reader.samples::<f32>() {
528                all_samples.push((sample.unwrap_or(0.0) * 32767.0) as i16);
529            }
530        }
531    }
532
533    // If stereo, convert to mono by averaging channels
534    if spec.channels == 2 {
535        let mono_samples = all_samples
536            .chunks(2)
537            .map(|chunk| ((chunk[0] as i32 + chunk[1] as i32) / 2) as i16)
538            .collect();
539        all_samples = mono_samples;
540    }
541    Ok((all_samples, spec.sample_rate))
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use std::io::Write;
548
549    #[tokio::test]
550    async fn test_wav_reader() -> Result<()> {
551        let file_path = "fixtures/sample.wav";
552        let file = File::open(file_path)?;
553        let mut reader = DecodedAudioReader::from_file(file, "wav", None, 16000)?;
554        let mut total_samples = 0;
555        let mut total_duration_ms = 0.0;
556        let mut chunk_count = 0;
557        while let Some((chunk, chunk_sample_rate)) = reader.read_chunk(20)? {
558            total_samples += chunk.len();
559            chunk_count += 1;
560            let chunk_duration_ms = (chunk.len() as f64 / chunk_sample_rate as f64) * 1000.0;
561            total_duration_ms += chunk_duration_ms;
562        }
563
564        let duration_seconds = total_duration_ms / 1000.0;
565        println!("Total chunks: {}", chunk_count);
566        println!("Actual samples: {}", total_samples);
567        println!("Actual duration: {:.2} seconds", duration_seconds);
568        assert_eq!(format!("{:.2}", duration_seconds), "7.51");
569        Ok(())
570    }
571    #[tokio::test]
572    async fn test_wav_file_track() -> Result<()> {
573        println!("Starting WAV file track test");
574
575        let file_path = "fixtures/sample.wav";
576        let file = File::open(file_path)?;
577
578        // First get the expected duration and samples using hound directly
579        let mut reader = hound::WavReader::new(File::open(file_path)?)?;
580        let spec = reader.spec();
581        let total_expected_samples = reader.duration() as usize;
582        let expected_duration = total_expected_samples as f64 / spec.sample_rate as f64;
583        println!("WAV file spec: {:?}", spec);
584        println!("Expected samples: {}", total_expected_samples);
585        println!("Expected duration: {:.2} seconds", expected_duration);
586
587        // Verify we can read all samples
588        let mut verify_samples = Vec::new();
589        for sample in reader.samples::<i16>() {
590            verify_samples.push(sample?);
591        }
592        println!("Verified total samples: {}", verify_samples.len());
593
594        let mut reader = DecodedAudioReader::from_file(file, "wav", None, 16000)?;
595        let mut total_samples = 0;
596        let mut total_duration_ms = 0.0;
597        let mut chunk_count = 0;
598
599        while let Some((chunk, chunk_sample_rate)) = reader.read_chunk(320)? {
600            total_samples += chunk.len();
601            chunk_count += 1;
602            // Calculate duration for this chunk
603            let chunk_duration_ms = (chunk.len() as f64 / chunk_sample_rate as f64) * 1000.0;
604            total_duration_ms += chunk_duration_ms;
605        }
606
607        let duration_seconds = total_duration_ms / 1000.0;
608        println!("Total chunks: {}", chunk_count);
609        println!("Actual samples: {}", total_samples);
610        println!("Actual duration: {:.2} seconds", duration_seconds);
611
612        // Allow for 1% tolerance in duration and sample count
613        const TOLERANCE: f64 = 0.01; // 1% tolerance
614
615        // If the file is stereo, we need to adjust the expected sample count
616        let expected_samples = if spec.channels == 2 {
617            total_expected_samples / 2 // We convert stereo to mono
618        } else {
619            total_expected_samples
620        };
621
622        assert!(
623            (duration_seconds - expected_duration).abs() < expected_duration * TOLERANCE,
624            "Duration {:.2} differs from expected {:.2} by more than {}%",
625            duration_seconds,
626            expected_duration,
627            TOLERANCE * 100.0
628        );
629
630        assert!(
631            (total_samples as f64 - expected_samples as f64).abs()
632                < expected_samples as f64 * TOLERANCE,
633            "Sample count {} differs from expected {} by more than {}%",
634            total_samples,
635            expected_samples,
636            TOLERANCE * 100.0
637        );
638
639        Ok(())
640    }
641
642    #[tokio::test]
643    async fn test_mp3_decode_sample_file() -> Result<()> {
644        let file_path = "fixtures/sample.mp3".to_string();
645        match File::open(&file_path) {
646            Ok(file) => {
647                let samples = crate::media::loader::decode_audio(file, "mp3", None, 16000)?;
648                assert!(
649                    !samples.is_empty(),
650                    "Decoded sample list should not be empty"
651                );
652            }
653            Err(_) => {
654                println!("Skipping MP3 test: sample file not found at {}", file_path);
655            }
656        }
657        Ok(())
658    }
659
660    #[tokio::test]
661    async fn test_mp3_file_track() -> Result<()> {
662        println!("Starting MP3 file track test");
663
664        // Check if the MP3 file exists
665        let file_path = "fixtures/sample.mp3".to_string();
666        let file = File::open(&file_path)?;
667        let sample_rate = 16000;
668        let mut reader = DecodedAudioReader::from_file(file, "mp3", None, sample_rate)?;
669        let mut total_samples = 0;
670        let mut total_duration_ms = 0.0;
671        while let Some((chunk, _chunk_sample_rate)) = reader.read_chunk(320)? {
672            total_samples += chunk.len();
673            // Calculate duration for this chunk
674            let chunk_duration_ms = (chunk.len() as f64 / sample_rate as f64) * 1000.0;
675            total_duration_ms += chunk_duration_ms;
676        }
677        let duration_seconds = total_duration_ms / 1000.0;
678        println!("Total samples: {}", total_samples);
679        println!("Duration: {:.2} seconds", duration_seconds);
680
681        const EXPECTED_SAMPLES: usize = 226310;
682        assert!(
683            total_samples == EXPECTED_SAMPLES,
684            "Sample count {} does not match expected {}",
685            total_samples,
686            EXPECTED_SAMPLES
687        );
688        Ok(())
689    }
690
691    #[tokio::test]
692    #[ignore = "manual debug helper: dumps raw pcm for ffplay"]
693    async fn dump_mp3_as_16k_pcm_for_ffplay() -> Result<()> {
694        let input_path = "fixtures/sample.mp3";
695        let output_path = "target/tmp/sample_16k_mono_s16le.pcm";
696
697        let file = File::open(input_path)?;
698        let mut reader = DecodedAudioReader::from_file(file, "mp3", None, 16000)?;
699
700        let mut pcm_samples = Vec::<i16>::new();
701        while let Some((chunk, _chunk_sample_rate)) = reader.read_chunk(320)? {
702            pcm_samples.extend_from_slice(&chunk);
703        }
704
705        std::fs::create_dir_all("target/tmp")?;
706        let mut out = std::fs::File::create(output_path)?;
707        for sample in pcm_samples {
708            out.write_all(&sample.to_le_bytes())?;
709        }
710        out.flush()?;
711
712        println!("PCM dumped: {}", output_path);
713        println!("ffplay -f s16le -ar 16000 -ac 1 {}", output_path);
714        Ok(())
715    }
716}