lumen-engine 0.2.2

Core composition model and rendering orchestration for Lumen.
Documentation
use std::{
    process::Command,
    sync::{Arc, Mutex},
};

use ffmpeg::media::Type;
use ffmpeg_next as ffmpeg;

use crate::{
    audio::{AUDIO_CHANNELS, AUDIO_SAMPLE_RATE, AudioBuffer, AudioMetadata, AudioResolver},
    error::MediaError,
};

use super::{ensure_ffmpeg_init, rational_to_f64};

pub struct FfmpegAudioResolver {
    id: String,
    metadata: AudioMetadata,
    decoded_ranges: Mutex<Vec<CachedAudioRange>>,
}

impl FfmpegAudioResolver {
    const DECODE_WINDOW_SAMPLES: usize = AUDIO_SAMPLE_RATE as usize * 30;

    pub fn open(source: impl Into<String>) -> Result<Self, MediaError> {
        ensure_ffmpeg_init()?;
        let source = source.into();
        let metadata = audio_metadata(&source)?;

        Ok(Self {
            id: source,
            metadata,
            decoded_ranges: Mutex::new(Vec::new()),
        })
    }

    fn cached_range(&self, start_sample: u64, frames: usize) -> Option<ResolvedAudioRange> {
        let cache = self.decoded_ranges.lock().ok()?;
        cache
            .iter()
            .find(|range| range.covers(start_sample, frames))
            .map(CachedAudioRange::resolved)
    }

    fn decode_range(
        &self,
        start_sample: u64,
        frames: usize,
    ) -> Result<ResolvedAudioRange, MediaError> {
        if let Some(range) = self.cached_range(start_sample, frames) {
            return Ok(range);
        }

        let frames_to_decode = frames.max(Self::DECODE_WINDOW_SAMPLES);
        let buffer = Arc::new(decode_audio_range_to_f32_stereo(
            &self.id,
            start_sample,
            frames_to_decode,
        )?);
        if let Ok(mut cache) = self.decoded_ranges.lock() {
            if let Some(existing) = cache
                .iter()
                .find(|range| range.covers(start_sample, frames))
            {
                return Ok(existing.resolved());
            }
            cache.push(CachedAudioRange {
                start_sample,
                buffer: Arc::clone(&buffer),
            });
        }

        Ok(ResolvedAudioRange {
            start_sample,
            buffer,
        })
    }
}

struct CachedAudioRange {
    start_sample: u64,
    buffer: Arc<AudioBuffer>,
}

impl CachedAudioRange {
    fn resolved(&self) -> ResolvedAudioRange {
        ResolvedAudioRange {
            start_sample: self.start_sample,
            buffer: Arc::clone(&self.buffer),
        }
    }

    fn covers(&self, start_sample: u64, frames: usize) -> bool {
        let end_sample = start_sample.saturating_add(frames as u64);
        let cached_end = self
            .start_sample
            .saturating_add(self.buffer.frames() as u64);
        start_sample >= self.start_sample && end_sample <= cached_end
    }
}

struct ResolvedAudioRange {
    start_sample: u64,
    buffer: Arc<AudioBuffer>,
}

impl AudioResolver for FfmpegAudioResolver {
    fn id(&self) -> &str {
        &self.id
    }

    fn metadata(&self) -> AudioMetadata {
        self.metadata
    }

    fn resolve_range(
        &self,
        start_sample: u64,
        frames: usize,
    ) -> Result<Arc<AudioBuffer>, MediaError> {
        let decoded = self.decode_range(start_sample, frames)?;
        let relative_start = start_sample.saturating_sub(decoded.start_sample);
        let start = usize::try_from(relative_start).unwrap_or(usize::MAX);
        let mut channels = vec![vec![0.0; frames]; AUDIO_CHANNELS];
        if start < decoded.buffer.frames() {
            let end = start.saturating_add(frames).min(decoded.buffer.frames());
            let copy_len = end.saturating_sub(start);
            for (channel_index, channel) in channels.iter_mut().enumerate() {
                let source = &decoded.buffer.channels()
                    [channel_index.min(decoded.buffer.channel_count().saturating_sub(1))];
                channel[..copy_len].copy_from_slice(&source[start..end]);
            }
        }

        Ok(Arc::new(AudioBuffer::from_channels(
            AUDIO_SAMPLE_RATE,
            channels,
        )))
    }
}

fn audio_metadata(source: &str) -> Result<AudioMetadata, MediaError> {
    let format = ffmpeg::format::input(source).map_err(|err| MediaError::Decode {
        media_source: source.to_string(),
        details: format!("failed opening audio source: {err}"),
    })?;
    let stream = format
        .streams()
        .best(Type::Audio)
        .ok_or_else(|| MediaError::SourceNotFound {
            media_source: source.to_string(),
        })?;
    let duration_seconds = if stream.duration() > 0 {
        rational_to_f64(stream.time_base())
            .map(|time_base| stream.duration() as f64 * time_base)
            .unwrap_or(0.0)
    } else if format.duration() > 0 {
        format.duration() as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE)
    } else {
        0.0
    };

    Ok(AudioMetadata {
        sample_rate: AUDIO_SAMPLE_RATE,
        channels: AUDIO_CHANNELS as u16,
        duration_samples: (duration_seconds.max(0.0) * f64::from(AUDIO_SAMPLE_RATE)).round() as u64,
    })
}

fn decode_audio_range_to_f32_stereo(
    source: &str,
    start_sample: u64,
    frames: usize,
) -> Result<AudioBuffer, MediaError> {
    let start_seconds = start_sample as f64 / f64::from(AUDIO_SAMPLE_RATE);
    let duration_seconds = frames as f64 / f64::from(AUDIO_SAMPLE_RATE);
    let output = Command::new("ffmpeg")
        .arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-ss")
        .arg(format!("{start_seconds:.9}"))
        .arg("-t")
        .arg(format!("{duration_seconds:.9}"))
        .arg("-i")
        .arg(source)
        .arg("-vn")
        .arg("-f")
        .arg("f32le")
        .arg("-acodec")
        .arg("pcm_f32le")
        .arg("-ac")
        .arg(AUDIO_CHANNELS.to_string())
        .arg("-ar")
        .arg(AUDIO_SAMPLE_RATE.to_string())
        .arg("pipe:1")
        .output()
        .map_err(|err| MediaError::Decode {
            media_source: source.to_string(),
            details: format!("failed spawning ffmpeg audio decoder: {err}"),
        })?;

    if !output.status.success() {
        return Err(MediaError::Decode {
            media_source: source.to_string(),
            details: format!(
                "ffmpeg audio decode failed: {}",
                String::from_utf8_lossy(&output.stderr)
            ),
        });
    }

    let sample_count = output.stdout.len() / std::mem::size_of::<f32>();
    let frame_count = sample_count / AUDIO_CHANNELS;
    let mut channels = vec![vec![0.0; frame_count]; AUDIO_CHANNELS];
    for frame in 0..frame_count {
        for (channel_index, channel) in channels.iter_mut().enumerate() {
            let byte_index = (frame * AUDIO_CHANNELS + channel_index) * 4;
            let bytes = [
                output.stdout[byte_index],
                output.stdout[byte_index + 1],
                output.stdout[byte_index + 2],
                output.stdout[byte_index + 3],
            ];
            channel[frame] = f32::from_le_bytes(bytes);
        }
    }

    Ok(AudioBuffer::from_channels(AUDIO_SAMPLE_RATE, channels))
}