Skip to main content

active_call/media/track/
file.rs

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