dreamwell-media 1.0.0

Dreamwell media pipeline — audio/video decode via FFmpeg, format conversion, streaming
Documentation
//! FFmpeg-based audio decoder — full codec support via ffmpeg-next.
//!
//! Decodes any audio format FFmpeg supports to mono f32 PCM.
//! Requires the `ffmpeg` feature flag.
//!
//! Supported formats: MP3, FLAC, AAC, Opus, OGG Vorbis, WMA, AIFF, M4A,
//! and every other codec FFmpeg compiles with.

use crate::audio_decode::{AudioBuffer, AudioDecodeError};
use std::path::Path;

/// Decode an audio file using FFmpeg demuxer + decoder + resampler.
///
/// Pipeline:
/// 1. Open container with avformat (demux)
/// 2. Find best audio stream
/// 3. Create decoder from stream codec parameters
/// 4. Decode all packets → PCM frames
/// 5. Resample to mono f32 at original sample rate
/// 6. Return AudioBuffer
pub fn decode_with_ffmpeg(path: &Path) -> Result<AudioBuffer, AudioDecodeError> {
    ffmpeg_next::init().map_err(|e| AudioDecodeError::DecodeFailed(format!("ffmpeg init: {}", e)))?;

    // Open input file
    let mut input = ffmpeg_next::format::input(&path)
        .map_err(|e| AudioDecodeError::IoError(format!("{}: {}", path.display(), e)))?;

    // Find best audio stream
    let audio_stream_index = input
        .streams()
        .best(ffmpeg_next::media::Type::Audio)
        .ok_or(AudioDecodeError::NoAudioStream)?
        .index();

    let stream = input
        .stream(audio_stream_index)
        .ok_or(AudioDecodeError::NoAudioStream)?;
    let codec_params = stream.parameters();

    let sample_rate;
    let channels;

    // Create decoder
    let context = ffmpeg_next::codec::context::Context::from_parameters(codec_params)
        .map_err(|e| AudioDecodeError::DecodeFailed(format!("codec context: {}", e)))?;
    let mut decoder = context
        .decoder()
        .audio()
        .map_err(|e| AudioDecodeError::DecodeFailed(format!("audio decoder: {}", e)))?;

    sample_rate = decoder.rate();
    channels = decoder.channels() as u16;

    // Set up resampler: input format → mono f32 at same sample rate
    let mut resampler = ffmpeg_next::software::resampling::context::Context::get(
        decoder.format(),
        decoder.channel_layout(),
        decoder.rate(),
        ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Packed),
        ffmpeg_next::ChannelLayout::MONO,
        decoder.rate(),
    )
    .map_err(|e| AudioDecodeError::DecodeFailed(format!("resampler: {}", e)))?;

    let mut all_samples: Vec<f32> = Vec::new();
    let mut decoded_frame = ffmpeg_next::frame::Audio::empty();

    // Decode all packets
    for (stream_idx, packet) in input.packets() {
        if stream_idx.index() != audio_stream_index {
            continue;
        }

        decoder
            .send_packet(&packet)
            .map_err(|e| AudioDecodeError::DecodeFailed(format!("send_packet: {}", e)))?;

        while decoder.receive_frame(&mut decoded_frame).is_ok() {
            // Resample to mono f32
            let mut resampled = ffmpeg_next::frame::Audio::empty();
            resampler
                .run(&decoded_frame, &mut resampled)
                .map_err(|e| AudioDecodeError::DecodeFailed(format!("resample: {}", e)))?;

            // Extract f32 samples from resampled frame
            let data = resampled.data(0);
            let sample_count = resampled.samples();
            let float_slice: &[f32] = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, sample_count) };
            all_samples.extend_from_slice(float_slice);
        }
    }

    // Flush decoder (send empty packet to get remaining frames)
    decoder
        .send_eof()
        .map_err(|e| AudioDecodeError::DecodeFailed(format!("send_eof: {}", e)))?;
    while decoder.receive_frame(&mut decoded_frame).is_ok() {
        let mut resampled = ffmpeg_next::frame::Audio::empty();
        resampler
            .run(&decoded_frame, &mut resampled)
            .map_err(|e| AudioDecodeError::DecodeFailed(format!("flush resample: {}", e)))?;
        let data = resampled.data(0);
        let sample_count = resampled.samples();
        let float_slice: &[f32] = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, sample_count) };
        all_samples.extend_from_slice(float_slice);
    }

    if all_samples.is_empty() {
        return Err(AudioDecodeError::DecodeFailed("decoded zero samples".into()));
    }

    let format_name = path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("unknown")
        .to_lowercase();

    log::info!(
        "FFmpeg decode: {} ({} samples, {}Hz, {}ch → mono f32)",
        path.display(),
        all_samples.len(),
        sample_rate,
        channels
    );

    Ok(AudioBuffer {
        samples: all_samples,
        sample_rate,
        original_channels: channels,
        format: format_name,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decode_nonexistent_ffmpeg() {
        let result = decode_with_ffmpeg(Path::new("nonexistent.mp3"));
        assert!(result.is_err());
    }
}