Skip to main content

active_call/media/
loader.rs

1use crate::media::cache;
2use anyhow::{Result, anyhow};
3use audio_codec::BoxedResampler;
4use audio_codec::opus::OpusDecoder;
5use hound::WavReader;
6use ogg::reading::PacketReader;
7use reqwest::Client;
8use std::fs::File;
9use std::io::{BufReader, Seek, SeekFrom, Write};
10use std::time::Instant;
11use symphonia::core::codecs::audio::AudioDecoderOptions;
12use symphonia::core::codecs::CodecParameters;
13use symphonia::core::errors::Error as SymphoniaError;
14use symphonia::core::formats::probe::Hint;
15use symphonia::core::formats::FormatOptions;
16use symphonia::core::formats::TrackType;
17use symphonia::core::io::MediaSourceStream;
18use symphonia::core::meta::MetadataOptions;
19use symphonia::default::{get_codecs, get_probe};
20use tracing::{info, warn};
21use url::Url;
22
23pub async fn download_from_url(url: &str, use_cache: bool) -> Result<(File, Option<String>)> {
24    let cache_key = cache::generate_cache_key(url, 0, None, None);
25    if use_cache && cache::is_cached(&cache_key).await? {
26        match cache::get_cache_path(&cache_key) {
27            Ok(path) => return Ok((File::open(&path).map_err(|e| anyhow!(e))?, None)),
28            Err(e) => {
29                warn!("loader: Error getting cache path: {}", e);
30                return Err(e);
31            }
32        }
33    }
34
35    let start_time = Instant::now();
36    let client = Client::new();
37    let response = client.get(url).send().await?;
38    let content_type = response
39        .headers()
40        .get(reqwest::header::CONTENT_TYPE)
41        .and_then(|v| v.to_str().ok())
42        .map(|s| s.split(';').next().unwrap_or(s).trim().to_string());
43    let bytes = response.bytes().await?;
44    let data = bytes.to_vec();
45    let duration = start_time.elapsed();
46
47    info!(
48        "loader: Downloaded {} bytes in {:?} for {} (content-type: {:?})",
49        data.len(),
50        duration,
51        url,
52        content_type,
53    );
54
55    if use_cache {
56        cache::store_in_cache(&cache_key, &data).await?;
57        match cache::get_cache_path(&cache_key) {
58            Ok(path) => return Ok((File::open(path).map_err(|e| anyhow!(e))?, content_type)),
59            Err(e) => {
60                warn!("loader: Error getting cache path: {}", e);
61                return Err(e);
62            }
63        }
64    }
65
66    let mut temp_file = tempfile::tempfile()?;
67    temp_file.write_all(&data)?;
68    temp_file.seek(SeekFrom::Start(0))?;
69    Ok((temp_file, content_type))
70}
71
72fn is_ogg(extension: &str, mime_type: Option<&str>) -> bool {
73    matches!(extension, "ogg" | "opus")
74        || matches!(
75            mime_type,
76            Some("audio/ogg") | Some("audio/opus") | Some("application/ogg")
77        )
78}
79
80enum OggCodec {
81    Opus { channels: u16 },
82    Other,
83}
84
85fn detect_ogg_codec(file: &mut File) -> Result<OggCodec> {
86    let mut reader = PacketReader::new(BufReader::new(&mut *file));
87    let head = reader
88        .read_packet_expected()
89        .map_err(|e| anyhow!("loader: failed reading OGG header: {e}"))?;
90    let codec = if head.data.starts_with(b"OpusHead") {
91        let channels = if head.data.len() > 9 {
92            head.data[9] as u16
93        } else {
94            2
95        };
96        OggCodec::Opus { channels }
97    } else {
98        OggCodec::Other
99    };
100    file.seek(SeekFrom::Start(0))?;
101    Ok(codec)
102}
103
104fn decode_opus_ogg(file: File, channels: u16, target_sample_rate: u32) -> Result<Vec<i16>> {
105    let mut reader = PacketReader::new(BufReader::new(file));
106
107    // Consume OpusHead (already peeked, but file was seeked back)
108    let head = reader
109        .read_packet_expected()
110        .map_err(|e| anyhow!("loader: failed reading OGG header: {e}"))?;
111    let channels = if head.data.len() > 9 {
112        head.data[9] as u16
113    } else {
114        channels
115    };
116
117    // Skip OpusTags packet
118    reader
119        .read_packet_expected()
120        .map_err(|e| anyhow!("loader: failed reading OpusTags: {e}"))?;
121
122    // Opus always encodes at 48 kHz; decode there and resample afterwards
123    let mut decoder = OpusDecoder::new(48000, channels);
124    let mut all_samples: Vec<i16> = Vec::new();
125
126    loop {
127        let packet = match reader.read_packet() {
128            Ok(Some(p)) => p,
129            Ok(None) => break,
130            Err(e) => return Err(anyhow!("loader: failed reading OGG packet: {e}")),
131        };
132        let samples = audio_codec::Decoder::decode(&mut decoder, &packet.data);
133        all_samples.extend_from_slice(&samples);
134    }
135
136    if all_samples.is_empty() {
137        return Err(anyhow!(
138            "loader: no decodable audio samples found in Opus stream"
139        ));
140    }
141
142    info!(
143        "loader: decoded Opus stream at 48000 Hz, {} channel(s)",
144        channels
145    );
146
147    if target_sample_rate != 48000 {
148        let mut resampler =
149            BoxedResampler::new(48000, target_sample_rate as usize).map_err(anyhow::Error::from)?;
150        all_samples = resampler.resample(&all_samples);
151    }
152
153    Ok(all_samples)
154}
155
156pub fn decode_wav(file: File, target_sample_rate: u32) -> Result<Vec<i16>> {
157    let reader = BufReader::new(file);
158    let mut wav_reader = WavReader::new(reader)?;
159    let spec = wav_reader.spec();
160    let sample_rate = spec.sample_rate;
161    let is_stereo = spec.channels == 2;
162
163    info!(
164        "WAV file detected with sample rate: {} Hz, channels: {}, bits: {}",
165        sample_rate, spec.channels, spec.bits_per_sample
166    );
167
168    let mut all_samples = Vec::new();
169
170    // Read all samples based on format and bit depth
171    match spec.sample_format {
172        hound::SampleFormat::Int => match spec.bits_per_sample {
173            16 => {
174                for sample in wav_reader.samples::<i16>() {
175                    if let Ok(s) = sample {
176                        all_samples.push(s);
177                    } else {
178                        break;
179                    }
180                }
181            }
182            8 => {
183                for sample in wav_reader.samples::<i8>() {
184                    if let Ok(s) = sample {
185                        all_samples.push((s as i16) * 256); // Convert 8-bit to 16-bit
186                    } else {
187                        break;
188                    }
189                }
190            }
191            24 | 32 => {
192                for sample in wav_reader.samples::<i32>() {
193                    if let Ok(s) = sample {
194                        all_samples.push((s >> 16) as i16); // Convert 24/32-bit to 16-bit
195                    } else {
196                        break;
197                    }
198                }
199            }
200            _ => {
201                return Err(anyhow!(
202                    "Unsupported bits per sample: {}",
203                    spec.bits_per_sample
204                ));
205            }
206        },
207        hound::SampleFormat::Float => {
208            for sample in wav_reader.samples::<f32>() {
209                if let Ok(s) = sample {
210                    all_samples.push((s * 32767.0) as i16); // Convert float to 16-bit
211                } else {
212                    break;
213                }
214            }
215        }
216    }
217
218    // Convert stereo to mono if needed
219    if is_stereo {
220        let mono_samples = all_samples
221            .chunks(2)
222            .map(|chunk| {
223                if chunk.len() == 2 {
224                    ((chunk[0] as i32 + chunk[1] as i32) / 2) as i16
225                } else {
226                    chunk[0]
227                }
228            })
229            .collect();
230        all_samples = mono_samples;
231    }
232
233    if sample_rate != target_sample_rate && sample_rate > 0 {
234        let mut resampler =
235            BoxedResampler::new(sample_rate as usize, target_sample_rate as usize)
236                .map_err(anyhow::Error::from)?;
237        all_samples = resampler.resample(&all_samples);
238    }
239
240    Ok(all_samples)
241}
242
243pub fn decode_audio(
244    mut file: File,
245    extension: &str,
246    mime_type: Option<&str>,
247    target_sample_rate: u32,
248) -> Result<Vec<i16>> {
249    if matches!(extension, "wav")
250        || matches!(
251            mime_type,
252            Some("audio/wav") | Some("audio/wave") | Some("audio/x-wav")
253        )
254    {
255        return decode_wav(file, target_sample_rate);
256    }
257
258    if is_ogg(extension, mime_type) {
259        match detect_ogg_codec(&mut file)? {
260            OggCodec::Opus { channels } => {
261                return decode_opus_ogg(file, channels, target_sample_rate);
262            }
263            OggCodec::Other => {} // fall through to symphonia (e.g. Vorbis)
264        }
265    }
266
267    let mss = MediaSourceStream::new(Box::new(file), Default::default());
268    let mut hint = Hint::new();
269    if !extension.is_empty() {
270        hint.with_extension(extension);
271    }
272    if let Some(mime) = mime_type {
273        hint.mime_type(mime);
274    }
275
276    let mut format = get_probe().probe(
277        &hint,
278        mss,
279        FormatOptions::default(),
280        MetadataOptions::default(),
281    )?;
282    let (track_id, audio_params) = {
283        let track = format
284            .default_track(TrackType::Audio)
285            .ok_or_else(|| anyhow!("loader: no default audio track found"))?;
286        let codec_params = track.codec_params
287            .as_ref()
288            .ok_or_else(|| anyhow!("loader: no codec parameters"))?;
289        let params = match codec_params {
290            CodecParameters::Audio(params) => params.clone(),
291            _ => return Err(anyhow!("loader: expected audio codec")),
292        };
293        (track.id, params)
294    };
295
296    let mut decoder = get_codecs().make_audio_decoder(&audio_params, &AudioDecoderOptions::default())?;
297    let mut all_samples = Vec::new();
298    let mut sample_rate = audio_params.sample_rate.unwrap_or(0);
299
300    loop {
301        let packet = match format.next_packet() {
302            Ok(Some(packet)) => packet,
303            Ok(None) => break,
304            Err(SymphoniaError::IoError(_)) => break,
305            Err(SymphoniaError::ResetRequired) => continue,
306            Err(e) => return Err(anyhow!("loader: failed reading audio packet: {e}")),
307        };
308
309        if packet.track_id != track_id {
310            continue;
311        }
312
313        match decoder.decode(&packet) {
314            Ok(decoded) => {
315                if sample_rate == 0 {
316                    sample_rate = decoded.spec().rate();
317                    info!(
318                        "loader: detected {:?} with sample rate: {} Hz, channels: {}",
319                        audio_params.codec,
320                        sample_rate,
321                        decoded.spec().channels().count()
322                    );
323                }
324                let channels = decoded.spec().channels().count();
325
326                let mut interleaved = Vec::new();
327                decoded.copy_to_vec_interleaved(&mut interleaved);
328
329                if channels <= 1 {
330                    all_samples.extend_from_slice(&interleaved);
331                } else {
332                    for frame in interleaved.chunks(channels) {
333                        if frame.is_empty() {
334                            continue;
335                        }
336                        let sum: i32 = frame.iter().map(|s| *s as i32).sum();
337                        all_samples.push((sum / frame.len() as i32) as i16);
338                    }
339                }
340            }
341            Err(SymphoniaError::DecodeError(_)) => continue,
342            Err(SymphoniaError::IoError(_)) => break,
343            Err(SymphoniaError::ResetRequired) => continue,
344            Err(e) => return Err(anyhow!("loader: failed decoding audio packet: {e}")),
345        }
346    }
347
348    if all_samples.is_empty() {
349        return Err(anyhow!("loader: no decodable audio samples found"));
350    }
351
352    if sample_rate != target_sample_rate && sample_rate > 0 {
353        let mut resampler =
354            BoxedResampler::new(sample_rate as usize, target_sample_rate as usize)
355                .map_err(anyhow::Error::from)?;
356        all_samples = resampler.resample(&all_samples);
357    }
358
359    Ok(all_samples)
360}
361
362/// Load audio from a path/URL, decode it to PCM at `target_sample_rate`, and
363/// cache the *decoded* PCM (not the original encoded file) so subsequent loads
364/// skip downloading and decoding entirely.
365///
366/// `offset_ms` skips that much audio from the start of the returned PCM. The
367/// full decoded PCM is still cached (the offset is not part of the cache key);
368/// on a cache hit only the bytes after the offset are read from disk.
369pub async fn load_audio_as_pcm_cached(
370    path: &str,
371    target_sample_rate: u32,
372    use_cache: bool,
373    offset_ms: u32,
374) -> Result<Vec<i16>> {
375    let cache_key = cache::generate_cache_key(path, target_sample_rate, None, None);
376    let offset_samples = (offset_ms as usize * target_sample_rate as usize) / 1000;
377
378    if use_cache && cache::is_cached(&cache_key).await? {
379        match cache::retrieve_pcm_from_cache_at(&cache_key, offset_samples).await {
380            Ok(samples) => {
381                info!(
382                    "loader: loaded {} decoded samples from pcm cache for {} (offset {} ms)",
383                    samples.len(),
384                    path,
385                    offset_ms
386                );
387                return Ok(samples);
388            }
389            Err(e) => warn!("loader: failed to read pcm cache for {}: {}", path, e),
390        }
391    }
392
393    let is_url = path.starts_with("http://") || path.starts_with("https://");
394
395    // Download/open the original file without caching the encoded bytes; we
396    // cache the decoded PCM below instead.
397    let (file, content_type) = if is_url {
398        download_from_url(path, false).await?
399    } else {
400        (
401            File::open(path).map_err(|e| anyhow!("loader: {} {}", path, e))?,
402            None,
403        )
404    };
405
406    let extension = if is_url {
407        path.parse::<Url>()?
408            .path()
409            .split('.')
410            .last()
411            .unwrap_or("")
412            .to_string()
413    } else {
414        path.split('.').last().unwrap_or("").to_string()
415    };
416
417    // Decoding is CPU-bound and blocking; keep it off the async runtime.
418    let mut samples = tokio::task::spawn_blocking(move || {
419        decode_audio(file, &extension, content_type.as_deref(), target_sample_rate)
420    })
421    .await??;
422
423    // Cache the full decoded PCM before applying the offset.
424    if use_cache {
425        if let Err(e) = cache::store_pcm_in_cache(&cache_key, &samples).await {
426            warn!("loader: failed to store pcm cache for {}: {}", path, e);
427        }
428    }
429
430    let skip = offset_samples.min(samples.len());
431    samples.drain(..skip);
432
433    Ok(samples)
434}
435
436pub async fn load_audio_as_pcm(
437    path: &str,
438    target_sample_rate: u32,
439    use_cache: bool,
440) -> Result<Vec<i16>> {
441    let is_url = path.starts_with("http://") || path.starts_with("https://");
442
443    let (file, content_type) = if is_url {
444        download_from_url(path, use_cache).await?
445    } else {
446        (File::open(path).map_err(|e| anyhow!("loader: {} {}", path, e))?, None)
447    };
448
449    let extension = if is_url {
450        path.parse::<Url>()?.path().split('.').last().unwrap_or("").to_string()
451    } else {
452        path.split('.').last().unwrap_or("").to_string()
453    };
454
455    decode_audio(
456        file,
457        &extension,
458        content_type.as_deref(),
459        target_sample_rate,
460    )
461}