use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use ffmpeg_next::channel_layout::ChannelLayout;
use ffmpeg_next::format::sample::{Sample, Type as SampleType};
use ffmpeg_next::frame::Audio as AudioFrame;
use ffmpeg_next::software::resampling;
use ffmpeg_next::Packet;
use crate::audio::format::AudioFormat;
use crate::audio::samples::AudioBuffer;
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TempFileGuard {
path: PathBuf,
}
impl TempFileGuard {
fn new(path: PathBuf) -> Self {
Self { path }
}
}
impl Drop for TempFileGuard {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
fn next_temp_path(ext: &str) -> PathBuf {
let count = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
temp_path_in(&std::env::temp_dir(), ext, count)
}
fn temp_path_in(dir: &std::path::Path, ext: &str, count: u64) -> PathBuf {
let pid = std::process::id();
dir.join(format!("rust_widgets_audio_enc_{pid}_{count}.{ext}"))
}
struct EncoderPlan {
muxer: &'static str,
encoders: &'static [&'static str],
bit_rate: i64,
}
fn format_to_ffmpeg_params(format: AudioFormat) -> Result<EncoderPlan, String> {
match format {
AudioFormat::Mp3 => {
Ok(EncoderPlan { muxer: "mp3", encoders: &["libmp3lame"], bit_rate: 192_000 })
}
AudioFormat::Flac => Ok(EncoderPlan { muxer: "flac", encoders: &["flac"], bit_rate: 0 }),
AudioFormat::Ogg => Ok(EncoderPlan {
muxer: "ogg",
encoders: &["libvorbis", "vorbis"],
bit_rate: 128_000,
}),
AudioFormat::Aac => {
Ok(EncoderPlan { muxer: "adts", encoders: &["aac"], bit_rate: 128_000 })
}
AudioFormat::Opus => {
Ok(EncoderPlan { muxer: "opus", encoders: &["libopus", "opus"], bit_rate: 64_000 })
}
_ => Err(format!("FFmpeg encoder does not support {:?}", format)),
}
}
fn resolve_encoder(
plan: &EncoderPlan,
) -> Result<(&'static str, ffmpeg_next::codec::codec::Codec), String> {
for name in plan.encoders {
if let Some(descriptor) = ffmpeg_next::encoder::find_by_name(name) {
return Ok((name, descriptor));
}
}
Err(format!(
"no encoder available for muxer '{}' (tried: {})",
plan.muxer,
plan.encoders.join(", ")
))
}
fn needs_experimental_opt_in(encoder_name: &str) -> bool {
matches!(encoder_name, "vorbis" | "opus")
}
fn choose_encoder_sample_format(codec_audio: &ffmpeg_next::codec::Audio) -> Sample {
let Some(formats) = codec_audio.formats() else {
return Sample::F32(SampleType::Packed);
};
let mut first_any: Option<Sample> = None;
for format in formats {
if format == Sample::F32(SampleType::Packed) {
return format;
}
if first_any.is_none() {
first_any = Some(format);
}
}
first_any.unwrap_or(Sample::F32(SampleType::Packed))
}
fn build_f32_frame(
buffer: &AudioBuffer,
buffer_offset: usize,
frame_samples: usize,
channel_layout: ChannelLayout,
pts: i64,
sample_rate: u32,
) -> Result<AudioFrame, String> {
let channels = buffer.channels as usize;
let mut frame = AudioFrame::new(Sample::F32(SampleType::Packed), frame_samples, channel_layout);
frame.set_rate(sample_rate);
frame.set_pts(Some(pts));
let dst = frame.data_mut(0);
let total = frame_samples * channels;
let end = (buffer_offset + total).min(buffer.samples.len());
let src = &buffer.samples[buffer_offset..end];
let src_bytes = unsafe { std::slice::from_raw_parts(src.as_ptr() as *const u8, src.len() * 4) };
let copy_len = dst.len().min(src_bytes.len());
dst[..copy_len].copy_from_slice(&src_bytes[..copy_len]);
Ok(frame)
}
pub fn ffmpeg_encode(buffer: &AudioBuffer, format: AudioFormat) -> Result<Vec<u8>, String> {
ffmpeg_next::init().map_err(|e| format!("FFmpeg init failed: {e}"))?;
let plan = format_to_ffmpeg_params(format)?;
let muxer_name = plan.muxer;
let sample_rate = buffer.sample_rate as i32;
let ext = format.extension();
let tmp_path = next_temp_path(ext);
let path_str = tmp_path.to_str().ok_or("Invalid temp file path")?.to_owned();
let _temp_guard = TempFileGuard::new(tmp_path.clone());
let mut octx = ffmpeg_next::format::output_as(&path_str, muxer_name)
.map_err(|e| format!("Failed to create muxer '{muxer_name}': {e}"))?;
let global = octx.format().flags().contains(ffmpeg_next::format::flag::Flags::GLOBAL_HEADER);
let (encoder_name, codec_descriptor) = resolve_encoder(&plan)?;
let codec_audio = codec_descriptor
.audio()
.map_err(|e| format!("'{encoder_name}' is not an audio encoder: {e}"))?;
let encoder_ctx = ffmpeg_next::codec::context::Context::new_with_codec(codec_descriptor);
let mut encoder_initial = encoder_ctx
.encoder()
.audio()
.map_err(|e| format!("Failed to create audio encoder: {e}"))?;
let channel_layout = match buffer.channels {
1 => ChannelLayout::MONO,
_ => ChannelLayout::STEREO,
};
let encoder_sample_rate = codec_audio
.rates()
.and_then(|mut r| r.find(|&rate| rate == sample_rate))
.unwrap_or(codec_audio.rates().and_then(|mut r| r.next()).unwrap_or(sample_rate));
if plan.bit_rate > 0 {
encoder_initial.set_bit_rate(plan.bit_rate as usize);
}
encoder_initial.set_rate(encoder_sample_rate);
encoder_initial.set_channel_layout(channel_layout);
let sample_format = choose_encoder_sample_format(&codec_audio);
encoder_initial.set_format(sample_format);
encoder_initial.set_time_base((1, encoder_sample_rate));
if global {
encoder_initial.set_flags(ffmpeg_next::codec::Flags::GLOBAL_HEADER);
}
if needs_experimental_opt_in(encoder_name) {
encoder_initial.compliance(ffmpeg_next::codec::Compliance::Experimental);
}
let mut encoder = encoder_initial
.open_as(codec_descriptor)
.map_err(|e| format!("Failed to open encoder '{encoder_name}': {e}"))?;
let actual_format = encoder.format();
let stream_index: usize;
{
let mut ost =
octx.add_stream(codec_descriptor).map_err(|e| format!("Failed to add stream: {e}"))?;
ost.set_parameters(&encoder);
stream_index = ost.index();
}
octx.write_header().map_err(|e| format!("Failed to write header: {e}"))?;
let src_rate = sample_rate as u32;
let src_layout = channel_layout;
let src_format = Sample::F32(SampleType::Packed);
let dst_rate = encoder_sample_rate as u32;
let dst_layout = channel_layout;
let dst_format = actual_format;
let needs_resample = dst_format != src_format || dst_rate != src_rate;
let mut resampler = if needs_resample {
Some(
resampling::Context::get(
src_format, src_layout, src_rate, dst_format, dst_layout, dst_rate,
)
.map_err(|e| format!("Failed to create resampler: {e}"))?,
)
} else {
None
};
let channels_us = buffer.channels as usize;
let frame_size = encoder.frame_size() as usize;
let samples_per_frame = if frame_size > 0 { frame_size } else { 1024 };
let total_samples = buffer.samples.len();
let mut sample_offset = 0;
let mut pts: i64 = 0;
while sample_offset < total_samples {
let samples_remaining = total_samples - sample_offset;
let frames_remaining = samples_remaining / channels_us;
let this_frame_samples = samples_per_frame.min(frames_remaining);
if this_frame_samples == 0 {
break;
}
let f32_frame = build_f32_frame(
buffer,
sample_offset,
this_frame_samples,
channel_layout,
pts,
sample_rate as u32,
)?;
let frame_to_send = if let Some(ref mut resampler) = resampler {
let mut converted = AudioFrame::empty();
resampler
.run(&f32_frame, &mut converted)
.map_err(|e| format!("Resampler error: {e}"))?;
converted.set_pts(f32_frame.pts());
converted
} else {
f32_frame
};
if let Err(e) = encoder.send_frame(&frame_to_send) {
return Err(format!("Send frame error (pts={}): {}", pts, e));
}
let mut packet = Packet::empty();
loop {
match encoder.receive_packet(&mut packet) {
Ok(()) => {
packet.set_stream(stream_index);
packet
.write_interleaved(&mut octx)
.map_err(|e| format!("Write packet error: {e}"))?;
}
Err(ffmpeg_next::Error::Eof) => break,
Err(ffmpeg_next::Error::Other { errno })
if std::io::Error::from_raw_os_error(errno).kind()
== std::io::ErrorKind::WouldBlock =>
{
break;
}
Err(e) => return Err(format!("Receive packet error (pts={pts}): {e}")),
}
packet = Packet::empty();
}
let samples_consumed = this_frame_samples * channels_us;
sample_offset += samples_consumed;
pts += this_frame_samples as i64;
}
encoder.send_eof().map_err(|e| format!("Failed to flush encoder: {e}"))?;
let is_flac = format == AudioFormat::Flac;
let mut dropped_packets: usize = 0;
let mut packet = Packet::empty();
loop {
match encoder.receive_packet(&mut packet) {
Ok(()) => {
packet.set_stream(stream_index);
if let Err(e) = packet.write_interleaved(&mut octx) {
if is_flac {
dropped_packets += 1;
log::warn!("FLAC flush: muxer rejected a trailing packet ({e}); dropped");
} else {
return Err(format!("Write packet error during flush: {e}"));
}
}
}
Err(ffmpeg_next::Error::Eof) => break,
Err(ffmpeg_next::Error::Other { errno })
if std::io::Error::from_raw_os_error(errno).kind()
== std::io::ErrorKind::WouldBlock =>
{
break;
}
Err(e) => return Err(format!("Receive packet error during flush: {e}")),
}
packet = Packet::empty();
}
if dropped_packets > 0 {
log::warn!("FLAC flush: dropped {dropped_packets} packet(s) rejected by the muxer");
}
octx.write_trailer().map_err(|e| format!("Write trailer error: {e}"))?;
let result = fs::read(&tmp_path).map_err(|e| format!("Failed to read output file: {e}"))?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::format::AudioFormat;
use crate::audio::samples::AudioBuffer;
#[test]
fn test_flac_encode_mono() {
let _serial = ENCODE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let samples: Vec<f32> = (0..4096)
.map(|i| (i as f32 / 44100.0 * 440.0 * 2.0 * std::f32::consts::PI).sin() * 0.5)
.collect();
let buf = AudioBuffer::new(44100, samples, 1);
let result = ffmpeg_encode(&buf, AudioFormat::Flac);
assert!(result.is_ok(), "FLAC mono 1-frame encoding failed: {:?}", result);
assert!(!result.unwrap().is_empty(), "FLAC mono output is empty");
}
#[test]
fn test_flac_encode_stereo_two_frames() {
let _serial = ENCODE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let samples: Vec<f32> = (0..16384)
.map(|i| (i as f32 / 44100.0 * 440.0 * 2.0 * std::f32::consts::PI).sin() * 0.5)
.collect();
let buf = AudioBuffer::new(44100, samples, 2);
let result = ffmpeg_encode(&buf, AudioFormat::Flac);
assert!(result.is_ok(), "FLAC stereo 2-frame encoding failed: {:?}", result);
assert!(!result.unwrap().is_empty(), "FLAC stereo output is empty");
}
#[test]
fn test_every_format_has_a_builtin_fallback_candidate() {
for format in [
AudioFormat::Mp3,
AudioFormat::Flac,
AudioFormat::Ogg,
AudioFormat::Aac,
AudioFormat::Opus,
] {
let plan = format_to_ffmpeg_params(format)
.unwrap_or_else(|e| panic!("{format:?} has no encoder plan: {e}"));
assert!(!plan.encoders.is_empty(), "{format:?} lists no encoder candidates");
assert!(
resolve_encoder(&plan).is_ok(),
"{format:?} has no available encoder among {:?} on this FFmpeg build",
plan.encoders
);
}
}
#[test]
fn test_experimental_opt_in_only_for_builtin_encoders() {
assert!(needs_experimental_opt_in("vorbis"));
assert!(needs_experimental_opt_in("opus"));
assert!(!needs_experimental_opt_in("libvorbis"));
assert!(!needs_experimental_opt_in("libopus"));
assert!(!needs_experimental_opt_in("aac"));
assert!(!needs_experimental_opt_in("flac"));
}
#[test]
fn test_resolve_encoder_reports_unavailable_candidates() {
let plan =
EncoderPlan { muxer: "ogg", encoders: &["definitely_not_a_real_encoder"], bit_rate: 0 };
let err = match resolve_encoder(&plan) {
Ok(_) => panic!("bogus encoder must not resolve"),
Err(err) => err,
};
assert!(err.contains("definitely_not_a_real_encoder"), "error was: {err}");
assert!(err.contains("ogg"), "error should name the muxer: {err}");
}
#[test]
fn test_ogg_encode_produces_real_ogg_container() {
let _serial = ENCODE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let samples: Vec<f32> = (0..16384)
.map(|i| (i as f32 / 44100.0 * 440.0 * 2.0 * std::f32::consts::PI).sin() * 0.5)
.collect();
let buf = AudioBuffer::new(44100, samples, 2);
let data = ffmpeg_encode(&buf, AudioFormat::Ogg)
.expect("Ogg encoding must succeed with either libvorbis or the built-in vorbis");
assert!(!data.is_empty(), "Ogg output is empty");
assert_eq!(
&data[..4],
b"OggS",
"Ogg output does not start with the OggS magic: {:02x?}",
&data[..4]
);
}
static ENCODE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn temp_file_guard_removes_file_on_drop() {
let path = temp_path_in(&std::env::temp_dir(), "guardprobe", u64::MAX - 1);
std::fs::write(&path, b"x").expect("write probe file");
assert!(path.exists(), "probe file should exist before the guard runs");
{
let _guard = TempFileGuard::new(path.clone());
assert!(path.exists(), "the guard must not delete the file early");
}
assert!(!path.exists(), "the guard must delete the file when dropped");
}
#[test]
fn test_failed_encode_leaves_no_temp_file() {
let _serial = ENCODE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let index = TEMP_COUNTER.load(Ordering::Relaxed);
let expected = temp_path_in(&std::env::temp_dir(), "mp3", index);
let buf = AudioBuffer::new(0, vec![0.0f32; 4096], 2);
let result = ffmpeg_encode(&buf, AudioFormat::Mp3);
assert!(result.is_err(), "a 0 Hz sample rate must fail to encode");
if TEMP_COUNTER.load(Ordering::Relaxed) != index + 1 {
return; }
assert!(!expected.exists(), "a failed encode leaked {expected:?}");
}
#[test]
fn test_successful_encode_leaves_no_temp_file() {
let _serial = ENCODE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let index = TEMP_COUNTER.load(Ordering::Relaxed);
let expected = temp_path_in(&std::env::temp_dir(), "mp3", index);
let samples: Vec<f32> = (0..16384)
.map(|i| (i as f32 / 44100.0 * 440.0 * 2.0 * std::f32::consts::PI).sin() * 0.5)
.collect();
let buf = AudioBuffer::new(44100, samples, 2);
ffmpeg_encode(&buf, AudioFormat::Mp3).expect("Mp3 encoding should succeed");
if TEMP_COUNTER.load(Ordering::Relaxed) != index + 1 {
return; }
assert!(!expected.exists(), "a successful encode leaked {expected:?}");
}
}